From 39e3fdc81c1fc05151ac49c2a9ca0d918bd63359 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sun, 3 Aug 2025 00:47:49 +0700 Subject: [PATCH] feat: sync payment methods --- .../payment_methods_remote_datasource.dart | 36 +- .../payment_methods_response_model.dart | 84 ++- .../payment_methods/payment_methods_bloc.dart | 9 +- .../home/pages/confirm_payment_page-old.dart | 76 ++- .../home/pages/confirm_payment_page.dart | 490 +++++++++--------- .../table/pages/payment_table_page.dart | 86 ++- 6 files changed, 410 insertions(+), 371 deletions(-) diff --git a/lib/data/datasources/payment_methods_remote_datasource.dart b/lib/data/datasources/payment_methods_remote_datasource.dart index 4ca105e..e602681 100644 --- a/lib/data/datasources/payment_methods_remote_datasource.dart +++ b/lib/data/datasources/payment_methods_remote_datasource.dart @@ -1,34 +1,40 @@ import 'dart:developer'; import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; import 'package:enaklo_pos/core/constants/variables.dart'; +import 'package:enaklo_pos/core/network/dio_client.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart'; -import 'package:http/http.dart' as http; class PaymentMethodsRemoteDatasource { - Future> getPaymentMethods() async { + final Dio dio = DioClient.instance; + Future> + getPaymentMethods() async { try { final authData = await AuthLocalDataSource().getAuthData(); - final response = await http.get( - Uri.parse('${Variables.baseUrl}/api/payment-methods'), - headers: { - 'Authorization': 'Bearer ${authData.token}', - 'Accept': 'application/json', - }, + final response = await dio.get( + '${Variables.baseUrl}/api/v1/payment-methods', + options: Options( + headers: { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + }, + ), ); - - log("Payment Methods Response Status: ${response.statusCode}"); - log("Payment Methods Response Body: ${response.body}"); - + if (response.statusCode == 200) { - return Right(PaymentMethodsResponseModel.fromJson(response.body)); + return Right(PaymentMethodsResponseModel.fromMap(response.data)); } else { return const Left('Failed to get payment methods'); } + } on DioException catch (e) { + log("Dio error: ${e.message}"); + return Left( + e.response?.data['message'] ?? 'Failed to get payment methods'); } catch (e) { log("Error getting payment methods: $e"); - return Left('Error: $e'); + return Left('Unexpected error'); } } -} \ No newline at end of file +} diff --git a/lib/data/models/response/payment_methods_response_model.dart b/lib/data/models/response/payment_methods_response_model.dart index ff08cb9..9207865 100644 --- a/lib/data/models/response/payment_methods_response_model.dart +++ b/lib/data/models/response/payment_methods_response_model.dart @@ -1,12 +1,14 @@ import 'dart:convert'; class PaymentMethodsResponseModel { - final String? status; - final List? data; + final bool? success; + final PaymentMethodsData? data; + final dynamic errors; PaymentMethodsResponseModel({ - this.status, + this.success, this.data, + this.errors, }); factory PaymentMethodsResponseModel.fromJson(String str) => @@ -16,51 +18,83 @@ class PaymentMethodsResponseModel { factory PaymentMethodsResponseModel.fromMap(Map json) => PaymentMethodsResponseModel( - status: json["status"], + success: json["success"], data: json["data"] == null - ? [] - : List.from( - json["data"]!.map((x) => PaymentMethod.fromMap(x))), + ? null + : PaymentMethodsData.fromMap(json["data"]), + errors: json["errors"], ); Map toMap() => { - "status": status, - "data": data == null + "success": success, + "data": data?.toMap(), + "errors": errors, + }; +} + +class PaymentMethodsData { + final List? paymentMethods; + final int? totalCount; + final int? page; + final int? limit; + final int? totalPages; + + PaymentMethodsData({ + this.paymentMethods, + this.totalCount, + this.page, + this.limit, + this.totalPages, + }); + + factory PaymentMethodsData.fromMap(Map json) => + PaymentMethodsData( + paymentMethods: json["payment_methods"] == null ? [] - : List.from(data!.map((x) => x.toMap())), + : List.from( + json["payment_methods"].map((x) => PaymentMethod.fromMap(x))), + totalCount: json["total_count"], + page: json["page"], + limit: json["limit"], + totalPages: json["total_pages"], + ); + + Map toMap() => { + "payment_methods": paymentMethods == null + ? [] + : List.from(paymentMethods!.map((x) => x.toMap())), + "total_count": totalCount, + "page": page, + "limit": limit, + "total_pages": totalPages, }; } class PaymentMethod { - final int? id; + final String? id; + final String? organizationId; final String? name; - final String? description; + final String? type; final bool? isActive; - final int? sortOrder; final DateTime? createdAt; final DateTime? updatedAt; PaymentMethod({ this.id, + this.organizationId, this.name, - this.description, + this.type, this.isActive, - this.sortOrder, this.createdAt, this.updatedAt, }); - factory PaymentMethod.fromJson(String str) => - PaymentMethod.fromMap(json.decode(str)); - - String toJson() => json.encode(toMap()); - factory PaymentMethod.fromMap(Map json) => PaymentMethod( id: json["id"], + organizationId: json["organization_id"], name: json["name"], - description: json["description"], + type: json["type"], isActive: json["is_active"], - sortOrder: json["sort_order"], createdAt: json["created_at"] == null ? null : DateTime.parse(json["created_at"]), @@ -71,11 +105,11 @@ class PaymentMethod { Map toMap() => { "id": id, + "organization_id": organizationId, "name": name, - "description": description, + "type": type, "is_active": isActive, - "sort_order": sortOrder, "created_at": createdAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(), }; -} \ No newline at end of file +} diff --git a/lib/presentation/home/bloc/payment_methods/payment_methods_bloc.dart b/lib/presentation/home/bloc/payment_methods/payment_methods_bloc.dart index b18d945..93b746d 100644 --- a/lib/presentation/home/bloc/payment_methods/payment_methods_bloc.dart +++ b/lib/presentation/home/bloc/payment_methods/payment_methods_bloc.dart @@ -8,17 +8,18 @@ part 'payment_methods_event.dart'; part 'payment_methods_state.dart'; part 'payment_methods_bloc.freezed.dart'; -class PaymentMethodsBloc extends Bloc { +class PaymentMethodsBloc + extends Bloc { final PaymentMethodsRemoteDatasource datasource; - + PaymentMethodsBloc(this.datasource) : super(const _Initial()) { on<_FetchPaymentMethods>((event, emit) async { emit(const _Loading()); final response = await datasource.getPaymentMethods(); response.fold( (l) => emit(_Error(l)), - (r) => emit(_Loaded(r.data ?? [])), + (r) => emit(_Loaded(r.data?.paymentMethods ?? [])), ); }); } -} \ No newline at end of file +} diff --git a/lib/presentation/home/pages/confirm_payment_page-old.dart b/lib/presentation/home/pages/confirm_payment_page-old.dart index 4d6b24a..c6b9676 100644 --- a/lib/presentation/home/pages/confirm_payment_page-old.dart +++ b/lib/presentation/home/pages/confirm_payment_page-old.dart @@ -72,11 +72,13 @@ class _ConfirmPaymentPageState extends State { // Set a default payment method in case API fails selectedPaymentMethod = PaymentMethod( - id: 1, - name: 'Cash', - description: 'Cash payment', + id: "4b1c0d21-c98a-4fc0-a2f9-8d90a0c9d905", + organizationId: "3e8b1793-d18b-40c4-a03d-0c6480b630c7", + name: "CASH", + type: "cash", isActive: true, - sortOrder: 1, + createdAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'), + updatedAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'), ); // if (selectTable == null && widget.table != null) { // selectTable = tables.firstWhere( @@ -1041,41 +1043,37 @@ class _ConfirmPaymentPageState extends State { 8.0), ) : null, - child: Tooltip( - message: method.description ?? - 'No description available', - child: isSelected - ? Button.filled( - width: double.infinity, - height: 50.0, - onPressed: () { - setState(() { - selectedPaymentMethod = - method; - }); - }, - label: method.name - ?.isNotEmpty == - true - ? method.name! - : 'Unknown', - ) - : Button.outlined( - width: double.infinity, - height: 50.0, - onPressed: () { - setState(() { - selectedPaymentMethod = - method; - }); - }, - label: method.name - ?.isNotEmpty == - true - ? method.name! - : 'Unknown', - ), - ), + child: isSelected + ? Button.filled( + width: double.infinity, + height: 50.0, + onPressed: () { + setState(() { + selectedPaymentMethod = + method; + }); + }, + label: method.name + ?.isNotEmpty == + true + ? method.name! + : 'Unknown', + ) + : Button.outlined( + width: double.infinity, + height: 50.0, + onPressed: () { + setState(() { + selectedPaymentMethod = + method; + }); + }, + label: method.name + ?.isNotEmpty == + true + ? method.name! + : 'Unknown', + ), ); }).toList(), ); diff --git a/lib/presentation/home/pages/confirm_payment_page.dart b/lib/presentation/home/pages/confirm_payment_page.dart index ee63f99..b0257d4 100644 --- a/lib/presentation/home/pages/confirm_payment_page.dart +++ b/lib/presentation/home/pages/confirm_payment_page.dart @@ -65,11 +65,13 @@ class _ConfirmPaymentPageState extends State { // Set a default payment method in case API fails selectedPaymentMethod = PaymentMethod( - id: 1, - name: 'Cash', - description: 'Cash payment', + id: "4b1c0d21-c98a-4fc0-a2f9-8d90a0c9d905", + organizationId: "3e8b1793-d18b-40c4-a03d-0c6480b630c7", + name: "CASH", + type: "cash", isActive: true, - sortOrder: 1, + createdAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'), + updatedAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'), ); // if (selectTable == null && widget.table != null) { // selectTable = tables.firstWhere( @@ -517,149 +519,149 @@ class _ConfirmPaymentPageState extends State { subtitle: 'Silahkan lakukan pembayaran', ), Expanded( - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.white, - border: Border( - bottom: BorderSide( - color: AppColors.grey, - width: 1.0, + child: SingleChildScrollView( + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + border: Border( + bottom: BorderSide( + color: AppColors.grey, + width: 1.0, + ), ), ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Pelanggan', - style: TextStyle( - color: AppColors.black, - fontSize: 16, - fontWeight: FontWeight.w600, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Pelanggan', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), - ), - const SpaceHeight(6.0), - TextFormField( - controller: customerController, - decoration: InputDecoration( - hintText: 'Nama Pelanggan', + const SpaceHeight(6.0), + TextFormField( + controller: customerController, + decoration: InputDecoration( + hintText: 'Nama Pelanggan', + ), + textCapitalization: + TextCapitalization.words, ), - textCapitalization: - TextCapitalization.words, - ), - ], - ), - ), - Container( - padding: const EdgeInsets.all(16), - width: double.infinity, - decoration: BoxDecoration( - color: AppColors.white, - border: Border( - bottom: BorderSide( - color: AppColors.grey, - width: 1.0, - ), + ], ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Metode Pembayaran', - style: TextStyle( - color: AppColors.black, - fontSize: 16, - fontWeight: FontWeight.w600, + Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + decoration: BoxDecoration( + color: AppColors.white, + border: Border( + bottom: BorderSide( + color: AppColors.grey, + width: 1.0, ), ), - const SpaceHeight(12.0), - BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () => const Center( - child: CircularProgressIndicator(), - ), - loading: () => const Center( - child: Column( + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Metode Pembayaran', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(12.0), + BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => const Center( + child: CircularProgressIndicator(), + ), + loading: () => const Center( + child: Column( + children: [ + CircularProgressIndicator(), + SizedBox(height: 8.0), + Text( + 'Loading payment methods...'), + ], + ), + ), + error: (message) => Column( children: [ - CircularProgressIndicator(), - SizedBox(height: 8.0), - Text( - 'Loading payment methods...'), + Center( + child: Text( + 'Error loading payment methods: $message'), + ), + const SpaceHeight(16.0), + Button.filled( + onPressed: () { + context + .read< + PaymentMethodsBloc>() + .add(PaymentMethodsEvent + .fetchPaymentMethods()); + }, + label: 'Retry', + ), ], ), - ), - error: (message) => Column( - children: [ - Center( - child: Text( - 'Error loading payment methods: $message'), - ), - const SpaceHeight(16.0), - Button.filled( - onPressed: () { - context - .read() - .add(PaymentMethodsEvent - .fetchPaymentMethods()); - }, - label: 'Retry', - ), - ], - ), - loaded: (paymentMethods) { - log("Loaded ${paymentMethods.length} payment methods"); - paymentMethods.forEach((method) { - log("Payment method: ${method.name} (ID: ${method.id})"); - }); - if (paymentMethods.isEmpty) { - return Column( - children: [ - const Center( - child: Text( - 'No payment methods available'), - ), - const SpaceHeight(16.0), - Button.filled( - onPressed: () { - context - .read< - PaymentMethodsBloc>() - .add(PaymentMethodsEvent - .fetchPaymentMethods()); - }, - label: 'Retry', - ), - ], - ); - } + loaded: (paymentMethods) { + log("Loaded ${paymentMethods.length} payment methods"); + paymentMethods.forEach((method) { + log("Payment method: ${method.name} (ID: ${method.id})"); + }); + if (paymentMethods.isEmpty) { + return Column( + children: [ + const Center( + child: Text( + 'No payment methods available'), + ), + const SpaceHeight(16.0), + Button.filled( + onPressed: () { + context + .read< + PaymentMethodsBloc>() + .add(PaymentMethodsEvent + .fetchPaymentMethods()); + }, + label: 'Retry', + ), + ], + ); + } - // Set default selected payment method if none selected or if current selection is not in the list - if (selectedPaymentMethod == null || - !paymentMethods.any((method) => - method.id == - selectedPaymentMethod?.id)) { - selectedPaymentMethod = - paymentMethods.first; - } + // Set default selected payment method if none selected or if current selection is not in the list + if (selectedPaymentMethod == null || + !paymentMethods.any((method) => + method.id == + selectedPaymentMethod + ?.id)) { + selectedPaymentMethod = + paymentMethods.first; + } - return Wrap( - spacing: 12.0, - runSpacing: 8.0, - children: - paymentMethods.map((method) { - final isSelected = - selectedPaymentMethod?.id == - method.id; - return Tooltip( - message: method.description ?? - 'No description available', - child: GestureDetector( + return Wrap( + spacing: 12.0, + runSpacing: 8.0, + children: + paymentMethods.map((method) { + final isSelected = + selectedPaymentMethod?.id == + method.id; + return GestureDetector( onTap: () { setState(() { selectedPaymentMethod = @@ -699,122 +701,124 @@ class _ConfirmPaymentPageState extends State { TextAlign.center, ), ), - ), - ); - }).toList(), - ); - }, - ); - }, - ), - ], - ), - ), - if (selectedPaymentMethod?.id == 1) - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.white, - border: Border( - bottom: BorderSide( - color: AppColors.grey, - width: 1.0, - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Total Bayar', - style: TextStyle( - color: AppColors.black, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - const SpaceHeight(8.0), - BlocBuilder( - builder: (context, state) { - return TextFormField( - controller: totalPriceController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: - BorderRadius.circular(8.0), - ), - hintText: 'Total harga', - ), - onChanged: (value) { - priceValue = - value.toIntegerFromText; - final int newValue = - value.toIntegerFromText; - totalPriceController.text = - newValue.currencyFormatRp; - totalPriceController.selection = - TextSelection.fromPosition( - TextPosition( - offset: - totalPriceController - .text.length)); + ); + }).toList(), + ); }, ); }, ), - const SpaceHeight(20.0), - BlocBuilder( - builder: (context, state) { - return Row( - children: [ - Button.outlined( - width: 150.0, - onPressed: () { - totalPriceController.text = - uangPas - .toString() - .currencyFormatRpV2; - priceValue = uangPas; - }, - label: 'UANG PAS', - ), - const SpaceWidth(20.0), - Button.outlined( - width: 150.0, - onPressed: () { - totalPriceController.text = - uangPas2 - .toString() - .currencyFormatRpV2; - priceValue = uangPas2; - }, - label: uangPas2 - .toString() - .currencyFormatRpV2, - ), - const SpaceWidth(20.0), - Button.outlined( - width: 150.0, - onPressed: () { - totalPriceController.text = - uangPas3 - .toString() - .currencyFormatRpV2; - priceValue = uangPas3; - }, - label: uangPas3 - .toString() - .currencyFormatRpV2, - ), - ], - ); - }, - ), ], ), ), - ], + if (selectedPaymentMethod?.type == "cash") + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + border: Border( + bottom: BorderSide( + color: AppColors.grey, + width: 1.0, + ), + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Total Bayar', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(8.0), + BlocBuilder( + builder: (context, state) { + return TextFormField( + controller: totalPriceController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(8.0), + ), + hintText: 'Total harga', + ), + onChanged: (value) { + priceValue = + value.toIntegerFromText; + final int newValue = + value.toIntegerFromText; + totalPriceController.text = + newValue.currencyFormatRp; + totalPriceController.selection = + TextSelection.fromPosition( + TextPosition( + offset: + totalPriceController + .text + .length)); + }, + ); + }, + ), + const SpaceHeight(20.0), + BlocBuilder( + builder: (context, state) { + return Row( + children: [ + Button.outlined( + width: 150.0, + onPressed: () { + totalPriceController.text = + uangPas + .toString() + .currencyFormatRpV2; + priceValue = uangPas; + }, + label: 'UANG PAS', + ), + const SpaceWidth(20.0), + Button.outlined( + width: 150.0, + onPressed: () { + totalPriceController.text = + uangPas2 + .toString() + .currencyFormatRpV2; + priceValue = uangPas2; + }, + label: uangPas2 + .toString() + .currencyFormatRpV2, + ), + const SpaceWidth(20.0), + Button.outlined( + width: 150.0, + onPressed: () { + totalPriceController.text = + uangPas3 + .toString() + .currencyFormatRpV2; + priceValue = uangPas3; + }, + label: uangPas3 + .toString() + .currencyFormatRpV2, + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), ), ), Container( diff --git a/lib/presentation/table/pages/payment_table_page.dart b/lib/presentation/table/pages/payment_table_page.dart index 8f32541..4619850 100644 --- a/lib/presentation/table/pages/payment_table_page.dart +++ b/lib/presentation/table/pages/payment_table_page.dart @@ -31,10 +31,10 @@ class PaymentTablePage extends StatefulWidget { final DraftOrderModel? draftOrder; final TableModel? table; const PaymentTablePage({ - Key? key, + super.key, this.draftOrder, this.table, - }) : super(key: key); + }); @override State createState() => _PaymentTablePageState(); @@ -98,11 +98,13 @@ class _PaymentTablePageState extends State { // Set a default payment method in case API fails selectedPaymentMethod = PaymentMethod( - id: 1, - name: 'Cash', - description: 'Cash payment', + id: "4b1c0d21-c98a-4fc0-a2f9-8d90a0c9d905", + organizationId: "3e8b1793-d18b-40c4-a03d-0c6480b630c7", + name: "CASH", + type: "cash", isActive: true, - sortOrder: 1, + createdAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'), + updatedAt: DateTime.tryParse('2025-07-18T03:43:13.857048+07:00'), ); } @@ -444,7 +446,7 @@ class _PaymentTablePageState extends State { ), BlocBuilder( builder: (context, state) { - final tax = state.maybeWhen( + state.maybeWhen( orElse: () => 0, loaded: (products, discountModel, @@ -805,41 +807,37 @@ class _PaymentTablePageState extends State { 8.0), ) : null, - child: Tooltip( - message: method.description ?? - 'No description available', - child: isSelected - ? Button.filled( - width: double.infinity, - height: 50.0, - onPressed: () { - setState(() { - selectedPaymentMethod = - method; - }); - }, - label: method.name - ?.isNotEmpty == - true - ? method.name! - : 'Unknown', - ) - : Button.outlined( - width: double.infinity, - height: 50.0, - onPressed: () { - setState(() { - selectedPaymentMethod = - method; - }); - }, - label: method.name - ?.isNotEmpty == - true - ? method.name! - : 'Unknown', - ), - ), + child: isSelected + ? Button.filled( + width: double.infinity, + height: 50.0, + onPressed: () { + setState(() { + selectedPaymentMethod = + method; + }); + }, + label: method.name + ?.isNotEmpty == + true + ? method.name! + : 'Unknown', + ) + : Button.outlined( + width: double.infinity, + height: 50.0, + onPressed: () { + setState(() { + selectedPaymentMethod = + method; + }); + }, + label: method.name + ?.isNotEmpty == + true + ? method.name! + : 'Unknown', + ), ); }).toList(), ); @@ -1052,7 +1050,7 @@ class _PaymentTablePageState extends State { if (discountModel == null) { return 0; } - return discountModel!.value! + return discountModel.value! .replaceAll('.00', '') .toIntegerFromText; }); @@ -1135,8 +1133,6 @@ class _PaymentTablePageState extends State { orderType, ); - final totalPrice = subTotal + finalTax; - return Flexible( child: Button.filled( onPressed: () async {