From bb9aef55cf9e894991f9e71be8320aaea2713cbe Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 5 Aug 2025 19:20:45 +0700 Subject: [PATCH] feat: create product --- lib/core/components/image_picker_widget.dart | 677 ++++++++- .../category_remote_datasource.dart | 45 +- .../datasources/file_remote_datasource.dart | 53 + .../product_remote_datasource.dart | 106 +- .../models/request/product_request_model.dart | 49 +- .../response/add_product_response_model.dart | 4 - .../response/category_response_model.dart | 169 ++- .../models/response/file_response_model.dart | 154 +++ lib/main.dart | 5 + .../bloc/add_product/add_product_bloc.dart | 17 +- .../add_product/add_product_bloc.freezed.dart | 51 +- .../bloc/add_product/add_product_event.dart | 2 +- .../get_categories/get_categories_bloc.dart | 5 +- .../update_product/update_product_bloc.dart | 57 +- .../update_product_bloc.freezed.dart | 58 +- .../update_product/update_product_event.dart | 5 +- .../bloc/upload_file/upload_file_bloc.dart | 29 + .../upload_file/upload_file_bloc.freezed.dart | 845 ++++++++++++ .../bloc/upload_file/upload_file_event.dart | 6 + .../bloc/upload_file/upload_file_state.dart | 9 + .../setting/dialogs/form_product_dialog.dart | 1207 ++++++++++------- .../setting/pages/sync_data_page.dart | 36 +- pubspec.lock | 8 + pubspec.yaml | 1 + 24 files changed, 2750 insertions(+), 848 deletions(-) create mode 100644 lib/data/datasources/file_remote_datasource.dart create mode 100644 lib/data/models/response/file_response_model.dart create mode 100644 lib/presentation/setting/bloc/upload_file/upload_file_bloc.dart create mode 100644 lib/presentation/setting/bloc/upload_file/upload_file_bloc.freezed.dart create mode 100644 lib/presentation/setting/bloc/upload_file/upload_file_event.dart create mode 100644 lib/presentation/setting/bloc/upload_file/upload_file_state.dart diff --git a/lib/core/components/image_picker_widget.dart b/lib/core/components/image_picker_widget.dart index 010fa6e..cc29a03 100644 --- a/lib/core/components/image_picker_widget.dart +++ b/lib/core/components/image_picker_widget.dart @@ -1,91 +1,189 @@ import 'dart:io'; +import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import '../assets/assets.gen.dart'; import '../constants/colors.dart'; import '../constants/variables.dart'; -import 'buttons.dart'; import 'spaces.dart'; class ImagePickerWidget extends StatefulWidget { final String label; final void Function(XFile? file) onChanged; + final void Function(String? uploadedUrl)? onUploaded; final bool showLabel; final String? initialImageUrl; + final bool autoUpload; const ImagePickerWidget({ super.key, required this.label, required this.onChanged, + this.onUploaded, this.showLabel = true, this.initialImageUrl, + this.autoUpload = false, }); @override State createState() => _ImagePickerWidgetState(); } -class _ImagePickerWidgetState extends State { +class _ImagePickerWidgetState extends State + with TickerProviderStateMixin { String? imagePath; + String? uploadedImageUrl; bool hasInitialImage = false; + bool isHovering = false; + bool isUploading = false; + late AnimationController _scaleController; + late AnimationController _fadeController; + late AnimationController _uploadController; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + late Animation _uploadAnimation; @override void initState() { super.initState(); hasInitialImage = widget.initialImageUrl != null; + + _scaleController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _uploadController = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _scaleController, + curve: Curves.easeInOut, + )); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _fadeController, + curve: Curves.easeInOut, + )); + + _uploadAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _uploadController, + curve: Curves.easeInOut, + )); + + _fadeController.forward(); + } + + @override + void dispose() { + _scaleController.dispose(); + _fadeController.dispose(); + _uploadController.dispose(); + super.dispose(); } Future _pickImage() async { + _scaleController.forward().then((_) { + _scaleController.reverse(); + }); + final pickedFile = await ImagePicker().pickImage( source: ImageSource.gallery, ); - setState(() { - if (pickedFile != null) { + if (pickedFile != null) { + setState(() { imagePath = pickedFile.path; - hasInitialImage = false; // Clear initial image when new image is picked - widget.onChanged(pickedFile); - } else { - debugPrint('No image selected.'); - widget.onChanged(null); + hasInitialImage = false; + uploadedImageUrl = null; + }); + + widget.onChanged(pickedFile); + + // Auto upload if enabled + if (widget.autoUpload) { + _uploadImage(pickedFile.path); } - }); + } else { + debugPrint('No image selected.'); + widget.onChanged(null); + } } - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.showLabel) ...[ - Text( - widget.label, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), + void _uploadImage(String filePath) { + setState(() { + isUploading = true; + }); + _uploadController.forward(); + + context.read().add( + UploadFileEvent.upload(filePath), + ); + } + + Widget _buildImageContainer() { + return Container( + width: 100.0, + height: 100.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), ), - const SpaceHeight(12.0), ], - Container( - padding: const EdgeInsets.all(6.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - border: Border.all(color: AppColors.primary), - ), - child: Row( - children: [ - SizedBox( - width: 80.0, - height: 80.0, - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: imagePath != null - ? Image.file( - File(imagePath!), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: Stack( + children: [ + Positioned.fill( + child: imagePath != null + ? Image.file( + File(imagePath!), + fit: BoxFit.cover, + ) + : uploadedImageUrl != null + ? CachedNetworkImage( + imageUrl: uploadedImageUrl!.contains('http') + ? uploadedImageUrl! + : '${Variables.baseUrl}/$uploadedImageUrl', + placeholder: (context, url) => Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary.withOpacity(0.1), + AppColors.primary.withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: const Center( + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + errorWidget: (context, url, error) => + _buildPlaceholder(), fit: BoxFit.cover, ) : hasInitialImage && widget.initialImageUrl != null @@ -93,38 +191,493 @@ class _ImagePickerWidgetState extends State { imageUrl: widget.initialImageUrl!.contains('http') ? widget.initialImageUrl! : '${Variables.baseUrl}/${widget.initialImageUrl}', - placeholder: (context, url) => - const Center(child: CircularProgressIndicator()), - errorWidget: (context, url, error) => Container( - padding: const EdgeInsets.all(16.0), - color: AppColors.black.withOpacity(0.05), - child: Assets.icons.image.svg(), + placeholder: (context, url) => Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary.withOpacity(0.1), + AppColors.primary.withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: const Center( + child: + CircularProgressIndicator(strokeWidth: 2), + ), ), + errorWidget: (context, url, error) => + _buildPlaceholder(), fit: BoxFit.cover, ) - : Container( - padding: const EdgeInsets.all(16.0), - color: AppColors.black.withOpacity(0.05), - child: Assets.icons.image.svg(), - ), + : _buildPlaceholder(), + ), + // Upload progress overlay + if (isUploading) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + value: _uploadAnimation.value == 1.0 + ? null + : _uploadAnimation.value, + ), + ), + const SizedBox(height: 8), + const Text( + 'Uploading...', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), ), ), - const Spacer(), - Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Button.filled( - height: 30.0, - width: 140.0, - onPressed: _pickImage, - label: 'Choose Photo', - fontSize: 12.0, - borderRadius: 5.0, + // Overlay gradient for better button visibility + if ((imagePath != null || + uploadedImageUrl != null || + (hasInitialImage && widget.initialImageUrl != null)) && + !isUploading) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.3), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), ), ), + ], + ), + ), + ); + } + + Widget _buildPlaceholder() { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary.withOpacity(0.1), + AppColors.primary.withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_photo_alternate_outlined, + size: 32, + color: AppColors.primary.withOpacity(0.6), + ), + const SizedBox(height: 4), + Text( + 'Photo', + style: TextStyle( + fontSize: 10, + color: AppColors.primary.withOpacity(0.6), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + Widget _buildActionButton() { + bool hasImage = imagePath != null || + uploadedImageUrl != null || + (hasInitialImage && widget.initialImageUrl != null); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: isUploading ? null : _pickImage, + onHover: (hover) { + setState(() { + isHovering = hover; + }); + }, + borderRadius: BorderRadius.circular(12), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: isHovering + ? Colors.white.withOpacity(0.1) + : Colors.transparent, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isUploading + ? Icons.cloud_upload_outlined + : hasImage + ? Icons.edit_outlined + : Icons.add_photo_alternate_outlined, + color: Colors.white, + size: 18, + ), + const SizedBox(width: 8), + Text( + isUploading + ? 'Uploading...' + : hasImage + ? 'Change Photo' + : 'Choose Photo', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildUploadButton() { + if (!widget.autoUpload && + imagePath != null && + uploadedImageUrl == null && + !isUploading) { + return Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + colors: [ + Colors.green.shade600, + Colors.green.shade500, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.green.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), ], ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _uploadImage(imagePath!), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.cloud_upload_outlined, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 8), + Text( + 'Upload to Server', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), ), - ], + ); + } + return const SizedBox.shrink(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + state.when( + initial: () {}, + loading: () { + if (!isUploading) { + setState(() { + isUploading = true; + }); + _uploadController.repeat(); + } + }, + success: (fileData) { + setState(() { + isUploading = false; + uploadedImageUrl = fileData.fileUrl; + }); + _uploadController.reset(); + + if (widget.onUploaded != null) { + widget.onUploaded!(fileData.fileUrl); + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 8), + Text('Image uploaded successfully!'), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + duration: Duration(seconds: 2), + ), + ); + }, + error: (message) { + setState(() { + isUploading = false; + }); + _uploadController.reset(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(Icons.error_outline, color: Colors.white), + SizedBox(width: 8), + Expanded(child: Text('Upload failed: $message')), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + duration: Duration(seconds: 3), + ), + ); + }, + ); + }, + child: FadeTransition( + opacity: _fadeAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.showLabel) ...[ + Text( + widget.label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.black87, + ), + ), + const SpaceHeight(16.0), + ], + ScaleTransition( + scale: _scaleAnimation, + child: Container( + padding: const EdgeInsets.all(20.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.0), + color: Colors.white, + border: Border.all( + color: isUploading + ? Colors.orange.withOpacity(0.5) + : uploadedImageUrl != null + ? Colors.green.withOpacity(0.5) + : AppColors.primary.withOpacity(0.2), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + _buildImageContainer(), + const SizedBox(height: 20), + _buildActionButton(), + _buildUploadButton(), + if ((imagePath != null || + uploadedImageUrl != null || + (hasInitialImage && + widget.initialImageUrl != null)) && + !isUploading) ...[ + const SizedBox(height: 12), + TextButton.icon( + onPressed: () { + setState(() { + imagePath = null; + hasInitialImage = false; + uploadedImageUrl = null; + }); + widget.onChanged(null); + if (widget.onUploaded != null) { + widget.onUploaded!(null); + } + }, + icon: Icon( + Icons.delete_outline, + size: 16, + color: Colors.red.shade400, + ), + label: Text( + 'Remove Photo', + style: TextStyle( + color: Colors.red.shade400, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + // Upload status indicator + if (uploadedImageUrl != null && !isUploading) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: + Border.all(color: Colors.green.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.cloud_done_outlined, + size: 14, + color: Colors.green.shade600, + ), + const SizedBox(width: 6), + Text( + 'Uploaded', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.green.shade600, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + ], + ), + ), ); } } + +/// Cara menggunakan widget ini: +/// +/// ```dart +/// // 1. Basic usage tanpa auto upload +/// ImagePickerWidget( +/// label: 'Product Image', +/// onChanged: (file) { +/// // Handle selected file +/// print('Selected file: ${file?.path}'); +/// }, +/// onUploaded: (url) { +/// // Handle uploaded URL +/// print('Uploaded URL: $url'); +/// }, +/// ) +/// +/// // 2. Auto upload setelah memilih gambar +/// ImagePickerWidget( +/// label: 'Profile Picture', +/// autoUpload: true, +/// onChanged: (file) => setState(() => selectedFile = file), +/// onUploaded: (url) => setState(() => profileImageUrl = url), +/// ) +/// +/// // 3. Dengan initial image +/// ImagePickerWidget( +/// label: 'Banner Image', +/// initialImageUrl: existingImageUrl, +/// onChanged: (file) => handleFileChange(file), +/// onUploaded: (url) => handleUploadSuccess(url), +/// ) +/// ``` +/// +/// Pastikan untuk wrap widget ini dengan BlocProvider: +/// ```dart +/// BlocProvider( +/// create: (context) => UploadFileBloc( +/// context.read(), +/// ), +/// child: ImagePickerWidget(...), +/// ) +/// ``` \ No newline at end of file diff --git a/lib/data/datasources/category_remote_datasource.dart b/lib/data/datasources/category_remote_datasource.dart index d81071a..1623496 100644 --- a/lib/data/datasources/category_remote_datasource.dart +++ b/lib/data/datasources/category_remote_datasource.dart @@ -1,27 +1,48 @@ 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/category_response_model.dart'; -import 'package:http/http.dart' as http; class CategoryRemoteDatasource { - Future> getCategories() async { + final Dio dio = DioClient.instance; + + Future> getCategories({ + int page = 1, + int limit = 10, + bool isActive = true, + }) async { final authData = await AuthLocalDataSource().getAuthData(); - final Map headers = { + final headers = { 'Authorization': 'Bearer ${authData.token}', 'Accept': 'application/json', }; - final response = await http.get( - Uri.parse('${Variables.baseUrl}/api/api-categories'), - headers: headers); - log(response.statusCode.toString()); - log(response.body); - if (response.statusCode == 200) { - return right(CategroyResponseModel.fromJson(response.body)); - } else { - return left(response.body); + + try { + final response = await dio.get( + '${Variables.baseUrl}/api/v1/categories', + queryParameters: { + 'page': page, + 'limit': limit, + 'is_active': isActive, + }, + options: Options(headers: headers), + ); + + if (response.statusCode == 200) { + return right(CategoryResponseModel.fromMap(response.data)); + } else { + return left(response.data.toString()); + } + } on DioException catch (e) { + log('Dio error: ${e.message}'); + return left(e.response?.data.toString() ?? e.message ?? 'Unknown error'); + } catch (e) { + log('Unexpected error: $e'); + return left('Unexpected error occurred'); } } } diff --git a/lib/data/datasources/file_remote_datasource.dart b/lib/data/datasources/file_remote_datasource.dart new file mode 100644 index 0000000..e46e70b --- /dev/null +++ b/lib/data/datasources/file_remote_datasource.dart @@ -0,0 +1,53 @@ +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/file_response_model.dart'; + +class FileRemoteDataSource { + final Dio dio = DioClient.instance; + + Future> uploadFile({ + required String filePath, + required String fileType, + required String description, + }) async { + final url = '${Variables.baseUrl}/api/v1/files/upload'; + + try { + final authData = await AuthLocalDataSource().getAuthData(); + + // Membuat FormData + final formData = FormData.fromMap({ + 'file': await MultipartFile.fromFile(filePath, + filename: filePath.split('/').last), + 'file_type': fileType, + 'description': description, + }); + + final response = await dio.post( + url, + data: formData, + options: Options( + headers: { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + // Content-Type otomatis diatur oleh Dio untuk FormData + }, + ), + ); + + if (response.statusCode == 201 || response.statusCode == 200) { + // Misal response.data['url'] adalah URL file yang diupload + return Right(FileResponseModel.fromJson(response.data)); + } else { + return Left('Upload gagal: ${response.statusMessage}'); + } + } on DioException catch (e) { + return Left(e.response?.data['message'] ?? 'Upload gagal'); + } catch (e) { + return Left('Unexpected error: $e'); + } + } +} diff --git a/lib/data/datasources/product_remote_datasource.dart b/lib/data/datasources/product_remote_datasource.dart index 5b2584f..5ce4e09 100644 --- a/lib/data/datasources/product_remote_datasource.dart +++ b/lib/data/datasources/product_remote_datasource.dart @@ -6,7 +6,6 @@ import 'package:enaklo_pos/core/network/dio_client.dart'; import 'package:enaklo_pos/data/models/request/product_request_model.dart'; import 'package:enaklo_pos/data/models/response/add_product_response_model.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; -import 'package:http/http.dart' as http; import '../../core/constants/variables.dart'; import 'auth_local_datasource.dart'; @@ -21,6 +20,10 @@ class ProductRemoteDatasource { final response = await dio.get( url, + queryParameters: { + 'page': 1, + 'limit': 30, + }, options: Options( headers: { 'Authorization': 'Bearer ${authData.token}', @@ -45,67 +48,64 @@ class ProductRemoteDatasource { Future> addProduct( ProductRequestModel productRequestModel) async { - final authData = await AuthLocalDataSource().getAuthData(); - final Map headers = { - 'Authorization': 'Bearer ${authData.token}', - }; - var request = http.MultipartRequest( - 'POST', Uri.parse('${Variables.baseUrl}/api/products')); - request.fields.addAll(productRequestModel.toMap()); - request.files.add(await http.MultipartFile.fromPath( - 'image', productRequestModel.image!.path)); - request.headers.addAll(headers); + try { + final authData = await AuthLocalDataSource().getAuthData(); + final url = '${Variables.baseUrl}/api/v1/products'; - http.StreamedResponse response = await request.send(); + final response = await dio.post( + url, + data: productRequestModel.toMap(), + options: Options( + headers: { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + }, + ), + ); - final String body = await response.stream.bytesToString(); - log(response.stream.toString()); - log(response.statusCode.toString()); - if (response.statusCode == 201) { - return right(AddProductResponseModel.fromJson(body)); - } else { - return left(body); + if (response.statusCode == 200) { + return Right(AddProductResponseModel.fromMap(response.data)); + } else { + return const Left('Failed to create products'); + } + } on DioException catch (e) { + log("Dio error: ${e.message}"); + return Left(e.response?.data['message'] ?? 'Gagal menambah produk'); + } catch (e) { + log("Unexpected error: $e"); + return const Left('Unexpected error occurred'); } } Future> updateProduct( ProductRequestModel productRequestModel) async { - final authData = await AuthLocalDataSource().getAuthData(); - final Map headers = { - 'Authorization': 'Bearer ${authData.token}', - }; + try { + final authData = await AuthLocalDataSource().getAuthData(); + final url = + '${Variables.baseUrl}/api/v1/products/${productRequestModel.id}'; - log("Update Product Request Data: ${productRequestModel.toMap()}"); - log("Update Product ID: ${productRequestModel.id}"); - log("Update Product Name: ${productRequestModel.name}"); - log("Update Product Price: ${productRequestModel.price}"); - log("Update Product Stock: ${productRequestModel.stock}"); - log("Update Product Category ID: ${productRequestModel.categoryId}"); - log("Update Product Is Best Seller: ${productRequestModel.isBestSeller}"); - log("Update Product Printer Type: ${productRequestModel.printerType}"); - log("Update Product Has Image: ${productRequestModel.image != null}"); + final response = await dio.put( + url, + data: productRequestModel.toMap(), + options: Options( + headers: { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + }, + ), + ); - var request = http.MultipartRequest( - 'POST', Uri.parse('${Variables.baseUrl}/api/products/edit')); - request.fields.addAll(productRequestModel.toMap()); - if (productRequestModel.image != null) { - request.files.add(await http.MultipartFile.fromPath( - 'image', productRequestModel.image!.path)); - } - request.headers.addAll(headers); - - log("Update Product Request Fields: ${request.fields}"); - log("Update Product Request Files: ${request.files.length}"); - - http.StreamedResponse response = await request.send(); - - final String body = await response.stream.bytesToString(); - log("Update Product Status Code: ${response.statusCode}"); - log("Update Product Body: $body"); - if (response.statusCode == 200) { - return right(AddProductResponseModel.fromJson(body)); - } else { - return left(body); + if (response.statusCode == 200) { + return Right(AddProductResponseModel.fromMap(response.data)); + } else { + return const Left('Failed to update products'); + } + } on DioException catch (e) { + log("Dio error: ${e.message}"); + return Left(e.response?.data['message'] ?? 'Gagal update produk'); + } catch (e) { + log("Unexpected error: $e"); + return const Left('Unexpected error occurred'); } } } diff --git a/lib/data/models/request/product_request_model.dart b/lib/data/models/request/product_request_model.dart index c0cfb9c..3e55e1a 100644 --- a/lib/data/models/request/product_request_model.dart +++ b/lib/data/models/request/product_request_model.dart @@ -1,40 +1,49 @@ -import 'dart:developer'; - -import 'package:image_picker/image_picker.dart'; - class ProductRequestModel { final String? id; final String name; + final String? description; + final String categoryId; + final String? sku; + final String? barcode; final int price; - final int stock; - final int categoryId; - final int isBestSeller; - final XFile? image; + final int cost; + final bool isActive; + final bool hasVariants; + final String imageUrl; final String? printerType; + ProductRequestModel({ this.id, required this.name, - required this.price, - required this.stock, + this.description, required this.categoryId, - required this.isBestSeller, - this.image, + this.sku, + this.barcode, + required this.price, + required this.cost, + this.isActive = true, + this.hasVariants = false, + required this.imageUrl, this.printerType, }); - Map toMap() { - log("toMap: $isBestSeller"); - final map = { + Map toMap() { + final map = { 'name': name, - 'price': price.toString(), - 'stock': stock.toString(), - 'category_id': categoryId.toString(), - 'is_best_seller': isBestSeller.toString(), + 'description': description ?? '', + 'category_id': categoryId, + 'sku': sku ?? '', + 'barcode': barcode ?? '', + 'price': price, + 'cost': cost, + 'is_active': isActive, + 'has_variants': hasVariants, + 'image_url': imageUrl, 'printer_type': printerType ?? '', }; if (id != null) { - map['id'] = id.toString(); + map['id'] = id; } return map; diff --git a/lib/data/models/response/add_product_response_model.dart b/lib/data/models/response/add_product_response_model.dart index 99e8af9..261ac28 100644 --- a/lib/data/models/response/add_product_response_model.dart +++ b/lib/data/models/response/add_product_response_model.dart @@ -4,12 +4,10 @@ import 'package:enaklo_pos/data/models/response/product_response_model.dart'; class AddProductResponseModel { final bool success; - final String message; final Product data; AddProductResponseModel({ required this.success, - required this.message, required this.data, }); @@ -21,13 +19,11 @@ class AddProductResponseModel { factory AddProductResponseModel.fromMap(Map json) => AddProductResponseModel( success: json["success"], - message: json["message"], data: Product.fromMap(json["data"]), ); Map toMap() => { "success": success, - "message": message, "data": data.toMap(), }; } diff --git a/lib/data/models/response/category_response_model.dart b/lib/data/models/response/category_response_model.dart index d50ea51..7755386 100644 --- a/lib/data/models/response/category_response_model.dart +++ b/lib/data/models/response/category_response_model.dart @@ -1,91 +1,120 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -class CategroyResponseModel { - final String status; - final List data; +class CategoryResponseModel { + final bool success; + final CategoryData data; + final dynamic errors; - CategroyResponseModel({ - required this.status, + CategoryResponseModel({ + required this.success, required this.data, + this.errors, }); - Map toMap() { - return { - 'status': status, - 'data': data.map((x) => x.toMap()).toList(), - }; - } - - factory CategroyResponseModel.fromMap(Map map) { - return CategroyResponseModel( - status: map['status'] as String, - data: List.from( - (map['data']).map( - (x) => CategoryModel.fromMap(x as Map), - ), - ), + factory CategoryResponseModel.fromMap(Map map) { + return CategoryResponseModel( + success: map['success'] as bool, + data: CategoryData.fromMap(map['data'] as Map), + errors: map['errors'], ); } - factory CategroyResponseModel.fromJson(String str) => - CategroyResponseModel.fromMap(json.decode(str)); - - String toJson() => json.encode(toMap()); -} - -class CategoryModel { - int? id; - String? name; - int? categoryId; - int? isSync; - String? image; - // DateTime createdAt; - // DateTime updatedAt; - - CategoryModel({this.id, this.name, this.categoryId, this.isSync, this.image}); + factory CategoryResponseModel.fromJson(String str) => + CategoryResponseModel.fromMap(json.decode(str)); Map toMap() { - return { - // 'id': id, - 'name': name, - 'is_sync': isSync ?? 1, - 'category_id': id, - 'image': image + return { + 'success': success, + 'data': data.toMap(), + 'errors': errors, }; } - factory CategoryModel.fromMap(Map map) { - return CategoryModel( - id: map['id'] as int?, - name: map['name'] as String?, - isSync: map['is_sync'] as int?, - categoryId: map['id'], - image: map['image']); - } - - factory CategoryModel.fromJson(String str) => - CategoryModel.fromMap(json.decode(str)); - String toJson() => json.encode(toMap()); +} - @override - bool operator ==(covariant CategoryModel other) { - if (identical(this, other)) return true; +class CategoryData { + final List categories; + final int totalCount; + final int page; + final int limit; + final int totalPages; - return other.id == id && - other.name == name && - other.categoryId == categoryId && - other.isSync == isSync && - other.image == image; + CategoryData({ + required this.categories, + required this.totalCount, + required this.page, + required this.limit, + required this.totalPages, + }); + + factory CategoryData.fromMap(Map map) { + return CategoryData( + categories: List.from( + (map['categories'] as List).map((x) => CategoryModel.fromMap(x)), + ), + totalCount: map['total_count'] as int, + page: map['page'] as int, + limit: map['limit'] as int, + totalPages: map['total_pages'] as int, + ); } - @override - int get hashCode { - return id.hashCode ^ - name.hashCode ^ - categoryId.hashCode ^ - isSync.hashCode ^ - image.hashCode; + Map toMap() { + return { + 'categories': categories.map((x) => x.toMap()).toList(), + 'total_count': totalCount, + 'page': page, + 'limit': limit, + 'total_pages': totalPages, + }; + } +} + +class CategoryModel { + String id; + final String organizationId; + final String name; + final String? description; + final String businessType; + final Map metadata; + final DateTime createdAt; + final DateTime updatedAt; + + CategoryModel({ + required this.id, + required this.organizationId, + required this.name, + this.description, + required this.businessType, + required this.metadata, + required this.createdAt, + required this.updatedAt, + }); + + factory CategoryModel.fromMap(Map map) { + return CategoryModel( + id: map['id'] as String, + organizationId: map['organization_id'] as String, + name: map['name'] as String, + description: map['description'] as String?, + businessType: map['business_type'] as String, + metadata: Map.from(map['metadata'] ?? {}), + createdAt: DateTime.parse(map['created_at'] as String), + updatedAt: DateTime.parse(map['updated_at'] as String), + ); + } + + Map toMap() { + return { + 'id': id, + 'organization_id': organizationId, + 'name': name, + 'description': description, + 'business_type': businessType, + 'metadata': metadata, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + }; } } diff --git a/lib/data/models/response/file_response_model.dart b/lib/data/models/response/file_response_model.dart new file mode 100644 index 0000000..777d67f --- /dev/null +++ b/lib/data/models/response/file_response_model.dart @@ -0,0 +1,154 @@ +class FileResponseModel { + final FileModel data; + final String message; + final bool success; + + FileResponseModel({ + required this.data, + required this.message, + required this.success, + }); + + factory FileResponseModel.fromJson(Map json) { + return FileResponseModel( + data: FileModel.fromJson(json['data']), + message: json['message'] as String, + success: json['success'] as bool, + ); + } + + Map toJson() => { + 'data': data.toJson(), + 'message': message, + 'success': success, + }; + + factory FileResponseModel.fromMap(Map map) => + FileResponseModel.fromJson(map); + + Map toMap() => toJson(); + + FileResponseModel copyWith({ + FileModel? data, + String? message, + bool? success, + }) { + return FileResponseModel( + data: data ?? this.data, + message: message ?? this.message, + success: success ?? this.success, + ); + } + + @override + String toString() => + 'FileResponseModel(data: $data, message: $message, success: $success)'; +} + +class FileModel { + final String id; + final String organizationId; + final String userId; + final String fileName; + final String originalName; + final String fileUrl; + final int fileSize; + final String mimeType; + final String fileType; + final String uploadPath; + final bool isPublic; + final DateTime createdAt; + final DateTime updatedAt; + + FileModel({ + required this.id, + required this.organizationId, + required this.userId, + required this.fileName, + required this.originalName, + required this.fileUrl, + required this.fileSize, + required this.mimeType, + required this.fileType, + required this.uploadPath, + required this.isPublic, + required this.createdAt, + required this.updatedAt, + }); + + factory FileModel.fromJson(Map json) { + return FileModel( + id: json['id'] as String, + organizationId: json['organization_id'] as String, + userId: json['user_id'] as String, + fileName: json['file_name'] as String, + originalName: json['original_name'] as String, + fileUrl: json['file_url'] as String, + fileSize: json['file_size'] as int, + mimeType: json['mime_type'] as String, + fileType: json['file_type'] as String, + uploadPath: json['upload_path'] as String, + isPublic: json['is_public'] as bool, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + ); + } + + Map toJson() => { + 'id': id, + 'organization_id': organizationId, + 'user_id': userId, + 'file_name': fileName, + 'original_name': originalName, + 'file_url': fileUrl, + 'file_size': fileSize, + 'mime_type': mimeType, + 'file_type': fileType, + 'upload_path': uploadPath, + 'is_public': isPublic, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + }; + + factory FileModel.fromMap(Map map) => + FileModel.fromJson(map); + + Map toMap() => toJson(); + + FileModel copyWith({ + String? id, + String? organizationId, + String? userId, + String? fileName, + String? originalName, + String? fileUrl, + int? fileSize, + String? mimeType, + String? fileType, + String? uploadPath, + bool? isPublic, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return FileModel( + id: id ?? this.id, + organizationId: organizationId ?? this.organizationId, + userId: userId ?? this.userId, + fileName: fileName ?? this.fileName, + originalName: originalName ?? this.originalName, + fileUrl: fileUrl ?? this.fileUrl, + fileSize: fileSize ?? this.fileSize, + mimeType: mimeType ?? this.mimeType, + fileType: fileType ?? this.fileType, + uploadPath: uploadPath ?? this.uploadPath, + isPublic: isPublic ?? this.isPublic, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + String toString() { + return 'FileModel(id: $id, organizationId: $organizationId, userId: $userId, fileName: $fileName, originalName: $originalName, fileUrl: $fileUrl, fileSize: $fileSize, mimeType: $mimeType, fileType: $fileType, uploadPath: $uploadPath, isPublic: $isPublic, createdAt: $createdAt, updatedAt: $updatedAt)'; + } +} diff --git a/lib/main.dart b/lib/main.dart index 1a06607..c7b7eb8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:enaklo_pos/core/constants/theme.dart'; import 'package:enaklo_pos/data/datasources/customer_remote_datasource.dart'; +import 'package:enaklo_pos/data/datasources/file_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/outlet_remote_data_source.dart'; import 'package:enaklo_pos/data/datasources/table_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/user_remote_datasource.dart'; @@ -14,6 +15,7 @@ import 'package:enaklo_pos/presentation/home/bloc/user_update_outlet/user_update import 'package:enaklo_pos/presentation/refund/bloc/refund_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart'; +import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart'; import 'package:enaklo_pos/presentation/void/bloc/void_order_bloc.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; @@ -265,6 +267,9 @@ class _MyAppState extends State { BlocProvider( create: (context) => UserUpdateOutletBloc(UserRemoteDatasource()), ), + BlocProvider( + create: (context) => UploadFileBloc(FileRemoteDataSource()), + ), ], child: MaterialApp( debugShowCheckedModeBanner: false, diff --git a/lib/presentation/setting/bloc/add_product/add_product_bloc.dart b/lib/presentation/setting/bloc/add_product/add_product_bloc.dart index 29bf3ef..e1885bb 100644 --- a/lib/presentation/setting/bloc/add_product/add_product_bloc.dart +++ b/lib/presentation/setting/bloc/add_product/add_product_bloc.dart @@ -1,11 +1,7 @@ -import 'dart:developer'; - import 'package:bloc/bloc.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/models/request/product_request_model.dart'; -import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:image_picker/image_picker.dart'; part 'add_product_event.dart'; part 'add_product_state.dart'; @@ -18,21 +14,12 @@ class AddProductBloc extends Bloc { ) : super(const _Initial()) { on<_AddProduct>((event, emit) async { emit(const _Loading()); - final requestData = ProductRequestModel( - name: event.product.name!, - price: event.product.price!, - stock: 0, - categoryId: 0, - isBestSeller: 0, - image: event.image, - ); - log("requestData: ${requestData.toString()}"); - final response = await datasource.addProduct(requestData); + final response = await datasource.addProduct(event.product); // products.add(newProduct); response.fold( (l) => emit(_Error(l)), (r) { - emit(_Success('Add Product Success')); + emit(_Success('Produk berhasil ditambahkan')); }, ); }); diff --git a/lib/presentation/setting/bloc/add_product/add_product_bloc.freezed.dart b/lib/presentation/setting/bloc/add_product/add_product_bloc.freezed.dart index a910a8e..9e6247b 100644 --- a/lib/presentation/setting/bloc/add_product/add_product_bloc.freezed.dart +++ b/lib/presentation/setting/bloc/add_product/add_product_bloc.freezed.dart @@ -19,19 +19,19 @@ mixin _$AddProductEvent { @optionalTypeArgs TResult when({ required TResult Function() started, - required TResult Function(Product product, XFile image) addProduct, + required TResult Function(ProductRequestModel product) addProduct, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? started, - TResult? Function(Product product, XFile image)? addProduct, + TResult? Function(ProductRequestModel product)? addProduct, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function()? started, - TResult Function(Product product, XFile image)? addProduct, + TResult Function(ProductRequestModel product)? addProduct, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -119,7 +119,7 @@ class _$StartedImpl implements _Started { @optionalTypeArgs TResult when({ required TResult Function() started, - required TResult Function(Product product, XFile image) addProduct, + required TResult Function(ProductRequestModel product) addProduct, }) { return started(); } @@ -128,7 +128,7 @@ class _$StartedImpl implements _Started { @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? started, - TResult? Function(Product product, XFile image)? addProduct, + TResult? Function(ProductRequestModel product)? addProduct, }) { return started?.call(); } @@ -137,7 +137,7 @@ class _$StartedImpl implements _Started { @optionalTypeArgs TResult maybeWhen({ TResult Function()? started, - TResult Function(Product product, XFile image)? addProduct, + TResult Function(ProductRequestModel product)? addProduct, required TResult orElse(), }) { if (started != null) { @@ -188,7 +188,7 @@ abstract class _$$AddProductImplCopyWith<$Res> { _$AddProductImpl value, $Res Function(_$AddProductImpl) then) = __$$AddProductImplCopyWithImpl<$Res>; @useResult - $Res call({Product product, XFile image}); + $Res call({ProductRequestModel product}); } /// @nodoc @@ -205,17 +205,12 @@ class __$$AddProductImplCopyWithImpl<$Res> @override $Res call({ Object? product = null, - Object? image = null, }) { return _then(_$AddProductImpl( null == product ? _value.product : product // ignore: cast_nullable_to_non_nullable - as Product, - null == image - ? _value.image - : image // ignore: cast_nullable_to_non_nullable - as XFile, + as ProductRequestModel, )); } } @@ -223,16 +218,14 @@ class __$$AddProductImplCopyWithImpl<$Res> /// @nodoc class _$AddProductImpl implements _AddProduct { - const _$AddProductImpl(this.product, this.image); + const _$AddProductImpl(this.product); @override - final Product product; - @override - final XFile image; + final ProductRequestModel product; @override String toString() { - return 'AddProductEvent.addProduct(product: $product, image: $image)'; + return 'AddProductEvent.addProduct(product: $product)'; } @override @@ -240,12 +233,11 @@ class _$AddProductImpl implements _AddProduct { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AddProductImpl && - (identical(other.product, product) || other.product == product) && - (identical(other.image, image) || other.image == image)); + (identical(other.product, product) || other.product == product)); } @override - int get hashCode => Object.hash(runtimeType, product, image); + int get hashCode => Object.hash(runtimeType, product); /// Create a copy of AddProductEvent /// with the given fields replaced by the non-null parameter values. @@ -259,29 +251,29 @@ class _$AddProductImpl implements _AddProduct { @optionalTypeArgs TResult when({ required TResult Function() started, - required TResult Function(Product product, XFile image) addProduct, + required TResult Function(ProductRequestModel product) addProduct, }) { - return addProduct(product, image); + return addProduct(product); } @override @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? started, - TResult? Function(Product product, XFile image)? addProduct, + TResult? Function(ProductRequestModel product)? addProduct, }) { - return addProduct?.call(product, image); + return addProduct?.call(product); } @override @optionalTypeArgs TResult maybeWhen({ TResult Function()? started, - TResult Function(Product product, XFile image)? addProduct, + TResult Function(ProductRequestModel product)? addProduct, required TResult orElse(), }) { if (addProduct != null) { - return addProduct(product, image); + return addProduct(product); } return orElse(); } @@ -319,11 +311,10 @@ class _$AddProductImpl implements _AddProduct { } abstract class _AddProduct implements AddProductEvent { - const factory _AddProduct(final Product product, final XFile image) = + const factory _AddProduct(final ProductRequestModel product) = _$AddProductImpl; - Product get product; - XFile get image; + ProductRequestModel get product; /// Create a copy of AddProductEvent /// with the given fields replaced by the non-null parameter values. diff --git a/lib/presentation/setting/bloc/add_product/add_product_event.dart b/lib/presentation/setting/bloc/add_product/add_product_event.dart index 39e69b7..229716a 100644 --- a/lib/presentation/setting/bloc/add_product/add_product_event.dart +++ b/lib/presentation/setting/bloc/add_product/add_product_event.dart @@ -3,6 +3,6 @@ part of 'add_product_bloc.dart'; @freezed class AddProductEvent with _$AddProductEvent { const factory AddProductEvent.started() = _Started; - const factory AddProductEvent.addProduct(Product product, XFile image) = + const factory AddProductEvent.addProduct(ProductRequestModel product) = _AddProduct; } diff --git a/lib/presentation/setting/bloc/get_categories/get_categories_bloc.dart b/lib/presentation/setting/bloc/get_categories/get_categories_bloc.dart index 18fdc6b..e7ab7f6 100644 --- a/lib/presentation/setting/bloc/get_categories/get_categories_bloc.dart +++ b/lib/presentation/setting/bloc/get_categories/get_categories_bloc.dart @@ -1,4 +1,3 @@ - import 'package:bloc/bloc.dart'; import 'package:enaklo_pos/data/datasources/category_remote_datasource.dart'; import 'package:enaklo_pos/data/models/response/category_response_model.dart'; @@ -15,11 +14,11 @@ class GetCategoriesBloc extends Bloc { ) : super(const _Initial()) { on<_Fetch>((event, emit) async { emit(const _Loading()); - final result = await datasource.getCategories(); + final result = await datasource.getCategories(limit: 50); result.fold( (l) => emit(_Error(l)), (r) async { - emit(_Success(r.data)); + emit(_Success(r.data.categories)); }, ); }); diff --git a/lib/presentation/setting/bloc/update_product/update_product_bloc.dart b/lib/presentation/setting/bloc/update_product/update_product_bloc.dart index 83ad348..6d3b625 100644 --- a/lib/presentation/setting/bloc/update_product/update_product_bloc.dart +++ b/lib/presentation/setting/bloc/update_product/update_product_bloc.dart @@ -1,12 +1,9 @@ import 'dart:developer'; import 'package:bloc/bloc.dart'; -import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/models/request/product_request_model.dart'; -import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:image_picker/image_picker.dart'; part 'update_product_event.dart'; part 'update_product_state.dart'; @@ -21,61 +18,11 @@ class UpdateProductBloc extends Bloc { emit(const _Loading()); try { - // Validate required fields - if (event.product.name == null || event.product.name!.isEmpty) { - emit(_Error('Product name is required')); - return; - } - - if (event.product.price == null || event.product.price == 0) { - emit(_Error('Product price is required')); - return; - } - - // if (event.product.stock == null) { - // emit(_Error('Product stock is required')); - // return; - // } - - if (event.product.categoryId == null) { - emit(_Error('Product category is required')); - return; - } - - // Parse price safely - final price = event.product.price!; - if (price == 0) { - emit(_Error('Invalid price format')); - return; - } - - final requestData = ProductRequestModel( - id: event.product.id, - name: event.product.name!, - price: price, - stock: 0, - categoryId: 0, - isBestSeller: 0, // Default to 0 if null - image: event.image, - printerType: 'kitchen', // Default to kitchen if null - ); - - log("Update requestData: ${requestData.toString()}"); - log("Request map: ${requestData.toMap()}"); - - final response = await datasource.updateProduct(requestData); + final response = await datasource.updateProduct(event.product); response.fold( (l) => emit(_Error(l)), (r) async { - // Update local database after successful API update - try { - await ProductLocalDatasource.instance - .updateProduct(event.product); - log("Local product updated successfully"); - } catch (e) { - log("Error updating local product: $e"); - } - emit(_Success('Update Product Success')); + emit(_Success('Product berhasil diupdate')); }, ); } catch (e) { diff --git a/lib/presentation/setting/bloc/update_product/update_product_bloc.freezed.dart b/lib/presentation/setting/bloc/update_product/update_product_bloc.freezed.dart index ad1c03a..5909ed1 100644 --- a/lib/presentation/setting/bloc/update_product/update_product_bloc.freezed.dart +++ b/lib/presentation/setting/bloc/update_product/update_product_bloc.freezed.dart @@ -16,21 +16,20 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$UpdateProductEvent { - Product get product => throw _privateConstructorUsedError; - XFile? get image => throw _privateConstructorUsedError; + ProductRequestModel get product => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ - required TResult Function(Product product, XFile? image) updateProduct, + required TResult Function(ProductRequestModel product) updateProduct, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(Product product, XFile? image)? updateProduct, + TResult? Function(ProductRequestModel product)? updateProduct, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ - TResult Function(Product product, XFile? image)? updateProduct, + TResult Function(ProductRequestModel product)? updateProduct, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -64,7 +63,7 @@ abstract class $UpdateProductEventCopyWith<$Res> { UpdateProductEvent value, $Res Function(UpdateProductEvent) then) = _$UpdateProductEventCopyWithImpl<$Res, UpdateProductEvent>; @useResult - $Res call({Product product, XFile? image}); + $Res call({ProductRequestModel product}); } /// @nodoc @@ -83,17 +82,12 @@ class _$UpdateProductEventCopyWithImpl<$Res, $Val extends UpdateProductEvent> @override $Res call({ Object? product = null, - Object? image = freezed, }) { return _then(_value.copyWith( product: null == product ? _value.product : product // ignore: cast_nullable_to_non_nullable - as Product, - image: freezed == image - ? _value.image - : image // ignore: cast_nullable_to_non_nullable - as XFile?, + as ProductRequestModel, ) as $Val); } } @@ -106,7 +100,7 @@ abstract class _$$UpdateProductImplCopyWith<$Res> __$$UpdateProductImplCopyWithImpl<$Res>; @override @useResult - $Res call({Product product, XFile? image}); + $Res call({ProductRequestModel product}); } /// @nodoc @@ -123,17 +117,12 @@ class __$$UpdateProductImplCopyWithImpl<$Res> @override $Res call({ Object? product = null, - Object? image = freezed, }) { return _then(_$UpdateProductImpl( null == product ? _value.product : product // ignore: cast_nullable_to_non_nullable - as Product, - freezed == image - ? _value.image - : image // ignore: cast_nullable_to_non_nullable - as XFile?, + as ProductRequestModel, )); } } @@ -141,16 +130,14 @@ class __$$UpdateProductImplCopyWithImpl<$Res> /// @nodoc class _$UpdateProductImpl implements _UpdateProduct { - const _$UpdateProductImpl(this.product, this.image); + const _$UpdateProductImpl(this.product); @override - final Product product; - @override - final XFile? image; + final ProductRequestModel product; @override String toString() { - return 'UpdateProductEvent.updateProduct(product: $product, image: $image)'; + return 'UpdateProductEvent.updateProduct(product: $product)'; } @override @@ -158,12 +145,11 @@ class _$UpdateProductImpl implements _UpdateProduct { return identical(this, other) || (other.runtimeType == runtimeType && other is _$UpdateProductImpl && - (identical(other.product, product) || other.product == product) && - (identical(other.image, image) || other.image == image)); + (identical(other.product, product) || other.product == product)); } @override - int get hashCode => Object.hash(runtimeType, product, image); + int get hashCode => Object.hash(runtimeType, product); /// Create a copy of UpdateProductEvent /// with the given fields replaced by the non-null parameter values. @@ -176,27 +162,27 @@ class _$UpdateProductImpl implements _UpdateProduct { @override @optionalTypeArgs TResult when({ - required TResult Function(Product product, XFile? image) updateProduct, + required TResult Function(ProductRequestModel product) updateProduct, }) { - return updateProduct(product, image); + return updateProduct(product); } @override @optionalTypeArgs TResult? whenOrNull({ - TResult? Function(Product product, XFile? image)? updateProduct, + TResult? Function(ProductRequestModel product)? updateProduct, }) { - return updateProduct?.call(product, image); + return updateProduct?.call(product); } @override @optionalTypeArgs TResult maybeWhen({ - TResult Function(Product product, XFile? image)? updateProduct, + TResult Function(ProductRequestModel product)? updateProduct, required TResult orElse(), }) { if (updateProduct != null) { - return updateProduct(product, image); + return updateProduct(product); } return orElse(); } @@ -231,13 +217,11 @@ class _$UpdateProductImpl implements _UpdateProduct { } abstract class _UpdateProduct implements UpdateProductEvent { - const factory _UpdateProduct(final Product product, final XFile? image) = + const factory _UpdateProduct(final ProductRequestModel product) = _$UpdateProductImpl; @override - Product get product; - @override - XFile? get image; + ProductRequestModel get product; /// Create a copy of UpdateProductEvent /// with the given fields replaced by the non-null parameter values. diff --git a/lib/presentation/setting/bloc/update_product/update_product_event.dart b/lib/presentation/setting/bloc/update_product/update_product_event.dart index 5fa93b6..a3356f1 100644 --- a/lib/presentation/setting/bloc/update_product/update_product_event.dart +++ b/lib/presentation/setting/bloc/update_product/update_product_event.dart @@ -2,5 +2,6 @@ part of 'update_product_bloc.dart'; @freezed class UpdateProductEvent with _$UpdateProductEvent { - const factory UpdateProductEvent.updateProduct(Product product, XFile? image) = _UpdateProduct; -} \ No newline at end of file + const factory UpdateProductEvent.updateProduct(ProductRequestModel product) = + _UpdateProduct; +} diff --git a/lib/presentation/setting/bloc/upload_file/upload_file_bloc.dart b/lib/presentation/setting/bloc/upload_file/upload_file_bloc.dart new file mode 100644 index 0000000..1652848 --- /dev/null +++ b/lib/presentation/setting/bloc/upload_file/upload_file_bloc.dart @@ -0,0 +1,29 @@ +import 'package:bloc/bloc.dart'; +import 'package:enaklo_pos/data/datasources/file_remote_datasource.dart'; +import 'package:enaklo_pos/data/models/response/file_response_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'upload_file_event.dart'; +part 'upload_file_state.dart'; +part 'upload_file_bloc.freezed.dart'; + +class UploadFileBloc extends Bloc { + final FileRemoteDataSource _fileRemoteDataSource; + UploadFileBloc(this._fileRemoteDataSource) + : super(UploadFileState.initial()) { + on<_Upload>((event, emit) async { + emit(_Loading()); + final result = await _fileRemoteDataSource.uploadFile( + filePath: event.filePath, + fileType: 'image', + description: 'Product Image', + ); + + result.fold((l) { + emit(_Error(l)); + }, (r) { + emit(_Success(r.data)); + }); + }); + } +} diff --git a/lib/presentation/setting/bloc/upload_file/upload_file_bloc.freezed.dart b/lib/presentation/setting/bloc/upload_file/upload_file_bloc.freezed.dart new file mode 100644 index 0000000..feec33b --- /dev/null +++ b/lib/presentation/setting/bloc/upload_file/upload_file_bloc.freezed.dart @@ -0,0 +1,845 @@ +// 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 'upload_file_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$UploadFileEvent { + String get filePath => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(String filePath) upload, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String filePath)? upload, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String filePath)? upload, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Upload value) upload, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Upload value)? upload, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Upload value)? upload, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + /// Create a copy of UploadFileEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UploadFileEventCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UploadFileEventCopyWith<$Res> { + factory $UploadFileEventCopyWith( + UploadFileEvent value, $Res Function(UploadFileEvent) then) = + _$UploadFileEventCopyWithImpl<$Res, UploadFileEvent>; + @useResult + $Res call({String filePath}); +} + +/// @nodoc +class _$UploadFileEventCopyWithImpl<$Res, $Val extends UploadFileEvent> + implements $UploadFileEventCopyWith<$Res> { + _$UploadFileEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UploadFileEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? filePath = null, + }) { + return _then(_value.copyWith( + filePath: null == filePath + ? _value.filePath + : filePath // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$UploadImplCopyWith<$Res> + implements $UploadFileEventCopyWith<$Res> { + factory _$$UploadImplCopyWith( + _$UploadImpl value, $Res Function(_$UploadImpl) then) = + __$$UploadImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String filePath}); +} + +/// @nodoc +class __$$UploadImplCopyWithImpl<$Res> + extends _$UploadFileEventCopyWithImpl<$Res, _$UploadImpl> + implements _$$UploadImplCopyWith<$Res> { + __$$UploadImplCopyWithImpl( + _$UploadImpl _value, $Res Function(_$UploadImpl) _then) + : super(_value, _then); + + /// Create a copy of UploadFileEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? filePath = null, + }) { + return _then(_$UploadImpl( + null == filePath + ? _value.filePath + : filePath // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$UploadImpl implements _Upload { + const _$UploadImpl(this.filePath); + + @override + final String filePath; + + @override + String toString() { + return 'UploadFileEvent.upload(filePath: $filePath)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UploadImpl && + (identical(other.filePath, filePath) || + other.filePath == filePath)); + } + + @override + int get hashCode => Object.hash(runtimeType, filePath); + + /// Create a copy of UploadFileEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UploadImplCopyWith<_$UploadImpl> get copyWith => + __$$UploadImplCopyWithImpl<_$UploadImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String filePath) upload, + }) { + return upload(filePath); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String filePath)? upload, + }) { + return upload?.call(filePath); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String filePath)? upload, + required TResult orElse(), + }) { + if (upload != null) { + return upload(filePath); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Upload value) upload, + }) { + return upload(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Upload value)? upload, + }) { + return upload?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Upload value)? upload, + required TResult orElse(), + }) { + if (upload != null) { + return upload(this); + } + return orElse(); + } +} + +abstract class _Upload implements UploadFileEvent { + const factory _Upload(final String filePath) = _$UploadImpl; + + @override + String get filePath; + + /// Create a copy of UploadFileEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UploadImplCopyWith<_$UploadImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$UploadFileState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(FileModel file) success, + required TResult Function(String message) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(FileModel file)? success, + TResult? Function(String message)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(FileModel file)? success, + TResult Function(String message)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UploadFileStateCopyWith<$Res> { + factory $UploadFileStateCopyWith( + UploadFileState value, $Res Function(UploadFileState) then) = + _$UploadFileStateCopyWithImpl<$Res, UploadFileState>; +} + +/// @nodoc +class _$UploadFileStateCopyWithImpl<$Res, $Val extends UploadFileState> + implements $UploadFileStateCopyWith<$Res> { + _$UploadFileStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UploadFileState + /// 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 _$UploadFileStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of UploadFileState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'UploadFileState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(FileModel file) success, + required TResult Function(String message) error, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(FileModel file)? success, + TResult? Function(String message)? error, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(FileModel file)? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements UploadFileState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$UploadFileStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of UploadFileState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'UploadFileState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(FileModel file) success, + required TResult Function(String message) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(FileModel file)? success, + TResult? Function(String message)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(FileModel file)? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements UploadFileState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$SuccessImplCopyWith<$Res> { + factory _$$SuccessImplCopyWith( + _$SuccessImpl value, $Res Function(_$SuccessImpl) then) = + __$$SuccessImplCopyWithImpl<$Res>; + @useResult + $Res call({FileModel file}); +} + +/// @nodoc +class __$$SuccessImplCopyWithImpl<$Res> + extends _$UploadFileStateCopyWithImpl<$Res, _$SuccessImpl> + implements _$$SuccessImplCopyWith<$Res> { + __$$SuccessImplCopyWithImpl( + _$SuccessImpl _value, $Res Function(_$SuccessImpl) _then) + : super(_value, _then); + + /// Create a copy of UploadFileState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? file = null, + }) { + return _then(_$SuccessImpl( + null == file + ? _value.file + : file // ignore: cast_nullable_to_non_nullable + as FileModel, + )); + } +} + +/// @nodoc + +class _$SuccessImpl implements _Success { + const _$SuccessImpl(this.file); + + @override + final FileModel file; + + @override + String toString() { + return 'UploadFileState.success(file: $file)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SuccessImpl && + (identical(other.file, file) || other.file == file)); + } + + @override + int get hashCode => Object.hash(runtimeType, file); + + /// Create a copy of UploadFileState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SuccessImplCopyWith<_$SuccessImpl> get copyWith => + __$$SuccessImplCopyWithImpl<_$SuccessImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(FileModel file) success, + required TResult Function(String message) error, + }) { + return success(file); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(FileModel file)? success, + TResult? Function(String message)? error, + }) { + return success?.call(file); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(FileModel file)? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (success != null) { + return success(file); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _Success implements UploadFileState { + const factory _Success(final FileModel file) = _$SuccessImpl; + + FileModel get file; + + /// Create a copy of UploadFileState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SuccessImplCopyWith<_$SuccessImpl> 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 _$UploadFileStateCopyWithImpl<$Res, _$ErrorImpl> + implements _$$ErrorImplCopyWith<$Res> { + __$$ErrorImplCopyWithImpl( + _$ErrorImpl _value, $Res Function(_$ErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of UploadFileState + /// 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 'UploadFileState.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 UploadFileState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + __$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(FileModel file) success, + required TResult Function(String message) error, + }) { + return error(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(FileModel file)? success, + TResult? Function(String message)? error, + }) { + return error?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(FileModel file)? success, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class _Error implements UploadFileState { + const factory _Error(final String message) = _$ErrorImpl; + + String get message; + + /// Create a copy of UploadFileState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/setting/bloc/upload_file/upload_file_event.dart b/lib/presentation/setting/bloc/upload_file/upload_file_event.dart new file mode 100644 index 0000000..c22bd69 --- /dev/null +++ b/lib/presentation/setting/bloc/upload_file/upload_file_event.dart @@ -0,0 +1,6 @@ +part of 'upload_file_bloc.dart'; + +@freezed +class UploadFileEvent with _$UploadFileEvent { + const factory UploadFileEvent.upload(String filePath) = _Upload; +} diff --git a/lib/presentation/setting/bloc/upload_file/upload_file_state.dart b/lib/presentation/setting/bloc/upload_file/upload_file_state.dart new file mode 100644 index 0000000..0e8de3d --- /dev/null +++ b/lib/presentation/setting/bloc/upload_file/upload_file_state.dart @@ -0,0 +1,9 @@ +part of 'upload_file_bloc.dart'; + +@freezed +class UploadFileState with _$UploadFileState { + const factory UploadFileState.initial() = _Initial; + const factory UploadFileState.loading() = _Loading; + const factory UploadFileState.success(FileModel file) = _Success; + const factory UploadFileState.error(String message) = _Error; +} diff --git a/lib/presentation/setting/dialogs/form_product_dialog.dart b/lib/presentation/setting/dialogs/form_product_dialog.dart index 1550d72..f709167 100644 --- a/lib/presentation/setting/dialogs/form_product_dialog.dart +++ b/lib/presentation/setting/dialogs/form_product_dialog.dart @@ -1,6 +1,8 @@ import 'dart:developer'; +import 'package:dropdown_search/dropdown_search.dart'; import 'package:enaklo_pos/core/components/custom_modal_dialog.dart'; +import 'package:enaklo_pos/data/models/request/product_request_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/components/custom_text_field.dart'; @@ -30,13 +32,17 @@ class FormProductDialog extends StatefulWidget { class _FormProductDialogState extends State { TextEditingController? nameController; + TextEditingController? descriptionController; + TextEditingController? skuController; + TextEditingController? barcodeController; TextEditingController? priceController; - TextEditingController? stockController; + TextEditingController? costController; XFile? imageFile; bool isBestSeller = false; int priceValue = 0; + int costValue = 0; CategoryModel? selectCategory; String? imageUrl; @@ -48,7 +54,10 @@ class _FormProductDialogState extends State { context.read().add(const GetCategoriesEvent.fetch()); nameController = TextEditingController(); priceController = TextEditingController(); - stockController = TextEditingController(); + descriptionController = TextEditingController(); + skuController = TextEditingController(); + barcodeController = TextEditingController(); + costController = TextEditingController(); // Check if we're in edit mode isEditMode = widget.product != null; @@ -58,11 +67,14 @@ class _FormProductDialogState extends State { final product = widget.product!; nameController!.text = product.name ?? ''; priceValue = product.price ?? 0; + costValue = product.cost ?? 0; priceController!.text = priceValue.currencyFormatRp; - stockController!.text = ''; + costController!.text = costValue.currencyFormatRp; + descriptionController!.text = product.description ?? ''; isBestSeller = false; printType = 'kitchen'; - imageUrl = ''; + imageUrl = product.imageUrl; + selectCategory!.id = product.categoryId.toString(); } super.initState(); @@ -73,7 +85,10 @@ class _FormProductDialogState extends State { super.dispose(); nameController!.dispose(); priceController!.dispose(); - stockController!.dispose(); + descriptionController!.dispose(); + skuController!.dispose(); + barcodeController!.dispose(); + costController!.dispose(); } @override @@ -97,6 +112,33 @@ class _FormProductDialogState extends State { textInputAction: TextInputAction.next, textCapitalization: TextCapitalization.words, ), + const SpaceHeight(20.0), + CustomTextField( + controller: descriptionController!, + label: 'Deskripsi', + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + textCapitalization: TextCapitalization.words, + ), + if (widget.product == null) ...[ + const SpaceHeight(20.0), + CustomTextField( + controller: skuController!, + label: 'SKU', + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + textCapitalization: TextCapitalization.words, + ), + const SpaceHeight(20.0), + CustomTextField( + controller: barcodeController!, + label: 'Barcode', + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + textCapitalization: TextCapitalization.words, + ), + ], + const SpaceHeight(20.0), CustomTextField( controller: priceController!, @@ -112,6 +154,20 @@ class _FormProductDialogState extends State { }, ), const SpaceHeight(20.0), + CustomTextField( + controller: costController!, + label: 'Cost', + keyboardType: TextInputType.number, + textInputAction: TextInputAction.next, + onChanged: (value) { + costValue = value.toIntegerFromText; + final int newValue = value.toIntegerFromText; + costController!.text = newValue.currencyFormatRp; + costController!.selection = TextSelection.fromPosition( + TextPosition(offset: costController!.text.length)); + }, + ), + const SpaceHeight(20.0), ImagePickerWidget( label: 'Foto Produk', onChanged: (file) { @@ -121,80 +177,291 @@ class _FormProductDialogState extends State { imageFile = file; }, initialImageUrl: imageUrl, + onUploaded: (uploadedUrl) => + setState(() => imageUrl = uploadedUrl), + autoUpload: true, ), const SpaceHeight(20.0), - CustomTextField( - controller: stockController!, - label: 'Stok', - keyboardType: TextInputType.number, - ), const SpaceHeight(20.0), - // const Text( - // "Kategori", - // style: TextStyle( - // fontSize: 14, - // fontWeight: FontWeight.w700, - // ), - // ), - // const SpaceHeight(12.0), - // BlocBuilder( - // builder: (context, state) { - // return state.maybeWhen( - // orElse: () { - // return const Center( - // child: CircularProgressIndicator(), - // ); - // }, - // success: (categories) { - // // Set the selected category if in edit mode and not already set - // if (isEditMode && - // selectCategory == null && - // widget.product?.category != null) { - // try { - // selectCategory = categories.firstWhere( - // (cat) => cat.id == widget.product!.category!.id, - // ); - // } catch (e) { - // // If no exact match found, leave selectCategory as null - // // This will show the hint text instead - // log("No matching category found for product category ID: ${widget.product!.category!.id}"); - // } - // } + const Text( + "Kategori", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.black87, + ), + ), + const SpaceHeight(12.0), + BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () { + return Container( + height: 56, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.blue, + ), + ), + ), + ); + }, + success: (categories) { + if (isEditMode == true) { + selectCategory = categories.firstWhere( + (item) => item.id == widget.product!.categoryId, + orElse: () => CategoryModel( + id: '', + organizationId: '', + name: '', + businessType: '', + metadata: {}, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ); + } + // Null safety check untuk categories + if (categories.isEmpty) { + return Container( + height: 56, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text( + "Tidak ada kategori tersedia", + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + ), + ); + } - // return DropdownButtonHideUnderline( - // child: Container( - // decoration: BoxDecoration( - // border: Border.all(color: Colors.grey), - // borderRadius: BorderRadius.circular(12), - // ), - // padding: const EdgeInsets.symmetric( - // horizontal: 10, vertical: 5), - // child: DropdownButton( - // value: selectCategory, - // hint: const Text("Pilih Kategori"), - // isExpanded: true, // Untuk mengisi lebar container - // onChanged: (newValue) { - // if (newValue != null) { - // selectCategory = newValue; - // setState(() {}); - // log("selectCategory: ${selectCategory!.name}"); - // } - // }, - // items: categories - // .map>( - // (CategoryModel category) { - // return DropdownMenuItem( - // value: category, - // child: Text(category.name!), - // ); - // }).toList(), - // ), - // ), - // ); - // }, - // ); - // }, - // ), + return DropdownSearch( + items: categories, + selectedItem: selectCategory, + + // Dropdown properties + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + hintText: "Pilih Kategori", + hintStyle: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + prefixIcon: Icon( + Icons.category_outlined, + color: Colors.grey.shade500, + size: 20, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade300, + width: 1.5, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade300, + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.blue.shade400, + width: 2, + ), + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + ), + + // Popup properties + popupProps: PopupProps.menu( + showSearchBox: true, + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Cari kategori...", + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + menuProps: MenuProps( + backgroundColor: Colors.white, + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + itemBuilder: (context, category, isSelected) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade50 + : Colors.transparent, + border: Border( + bottom: BorderSide( + color: Colors.grey.shade100, + width: 0.5, + ), + ), + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: isSelected + ? Colors.blue.shade600 + : Colors.grey.shade400, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + category.name, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w500, + color: isSelected + ? Colors.blue.shade700 + : Colors.black87, + ), + ), + ), + if (isSelected) + Icon( + Icons.check, + color: Colors.blue.shade600, + size: 18, + ), + ], + ), + ); + }, + emptyBuilder: (context, searchEntry) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.search_off, + color: Colors.grey.shade400, + size: 48, + ), + const SizedBox(height: 12), + Text( + searchEntry.isEmpty + ? "Tidak ada kategori tersedia" + : "Tidak ditemukan kategori dengan '${searchEntry}'", + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + }, + ), + + // Item as string (for search functionality) + itemAsString: (CategoryModel category) => + category.name, + + // Comparison function + compareFn: + (CategoryModel? item1, CategoryModel? item2) { + return item1?.id == item2?.id; + }, + + // On changed callback + onChanged: (CategoryModel? selectedCategory) { + if (selectedCategory != null) { + setState(() { + selectCategory = selectedCategory; + }); + log("selectCategory: ${selectCategory!.name}"); + } + }, + + // Validator (optional) + validator: (CategoryModel? value) { + if (value == null) { + return "Kategori harus dipilih"; + } + return null; + }, + ); + }, + error: (message) { + return Container( + height: 56, + decoration: BoxDecoration( + border: Border.all(color: Colors.red.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + color: Colors.red.shade400, + size: 20, + ), + const SizedBox(width: 8), + Text( + "Gagal memuat kategori", + style: TextStyle( + color: Colors.red.shade600, + fontSize: 14, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), const SpaceHeight(12.0), const Text( "Tipe Print", @@ -298,18 +565,20 @@ class _FormProductDialogState extends State { log("isBestSeller: $isBestSeller"); final String name = nameController!.text; - final int stock = - stockController!.text.toIntegerFromText; - final Product product = widget.product!.copyWith( + final ProductRequestModel product = + ProductRequestModel( + id: widget.product!.id, name: name, + description: descriptionController!.text, price: priceValue, + cost: costValue, + categoryId: selectCategory!.id, printerType: printType, + imageUrl: imageUrl.toString(), ); - context.read().add( - UpdateProductEvent.updateProduct( - product, imageFile)); + UpdateProductEvent.updateProduct(product)); }, label: 'Ubah Produk', ); @@ -364,17 +633,22 @@ class _FormProductDialogState extends State { log("isBestSeller: $isBestSeller"); final String name = nameController!.text; - final int stock = - stockController!.text.toIntegerFromText; - final Product product = Product( - name: name, - price: priceValue, - imageUrl: imageFile!.path, - printerType: printType, - ); - context.read().add( - AddProductEvent.addProduct( - product, imageFile!)); + final ProductRequestModel product = + ProductRequestModel( + name: name, + price: priceValue, + printerType: printType, + categoryId: selectCategory!.id, + cost: costValue, + imageUrl: imageUrl.toString(), + barcode: barcodeController!.text, + sku: skuController!.text, + description: descriptionController!.text); + context + .read() + .add(AddProductEvent.addProduct( + product, + )); }, label: 'Simpan Produk', ); @@ -397,396 +671,397 @@ class _FormProductDialogState extends State { } } -class FormProductDialogOld extends StatefulWidget { - final Product? product; - const FormProductDialogOld({ - super.key, - this.product, - }); +// class FormProductDialogOld extends StatefulWidget { +// final Product? product; +// const FormProductDialogOld({ +// super.key, +// this.product, +// }); - @override - State createState() => _FormProductDialogOldState(); -} +// @override +// State createState() => _FormProductDialogOldState(); +// } -class _FormProductDialogOldState extends State { - TextEditingController? nameController; - TextEditingController? priceController; - TextEditingController? stockController; +// class _FormProductDialogOldState extends State { +// TextEditingController? nameController; +// TextEditingController? priceController; +// TextEditingController? stockController; - XFile? imageFile; +// XFile? imageFile; - bool isBestSeller = false; - int priceValue = 0; +// bool isBestSeller = false; +// int priceValue = 0; +// int costValue = 0; - CategoryModel? selectCategory; - String? imageUrl; - String? printType = 'kitchen'; - bool isEditMode = false; +// CategoryModel? selectCategory; +// String? imageUrl; +// String? printType = 'kitchen'; +// bool isEditMode = false; - @override - void initState() { - context.read().add(const GetCategoriesEvent.fetch()); - nameController = TextEditingController(); - priceController = TextEditingController(); - stockController = TextEditingController(); +// @override +// void initState() { +// context.read().add(const GetCategoriesEvent.fetch()); +// nameController = TextEditingController(); +// priceController = TextEditingController(); +// stockController = TextEditingController(); - // Check if we're in edit mode - isEditMode = widget.product != null; +// // Check if we're in edit mode +// isEditMode = widget.product != null; - if (isEditMode) { - // Pre-fill the form with existing product data - // final product = widget.product!; - // nameController!.text = product.name ?? ''; - // priceValue = int.tryParse(product.price ?? '0') ?? 0; - // priceController!.text = priceValue.currencyFormatRp; - // stockController!.text = (product.stock ?? 0).toString(); - // isBestSeller = product.isFavorite == 1; - // printType = product.printerType ?? 'kitchen'; - // imageUrl = product.image; - } +// if (isEditMode) { +// // Pre-fill the form with existing product data +// // final product = widget.product!; +// // nameController!.text = product.name ?? ''; +// // priceValue = int.tryParse(product.price ?? '0') ?? 0; +// // priceController!.text = priceValue.currencyFormatRp; +// // stockController!.text = (product.stock ?? 0).toString(); +// // isBestSeller = product.isFavorite == 1; +// // printType = product.printerType ?? 'kitchen'; +// // imageUrl = product.image; +// } - super.initState(); - } +// super.initState(); +// } - @override - void dispose() { - super.dispose(); - nameController!.dispose(); - priceController!.dispose(); - stockController!.dispose(); - } +// @override +// void dispose() { +// super.dispose(); +// nameController!.dispose(); +// priceController!.dispose(); +// stockController!.dispose(); +// } - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - onPressed: () => context.pop(), - icon: const Icon(Icons.close), - ), - Text( - isEditMode ? 'Edit Product' : 'Add Product', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const Spacer(), - ], - ), - content: SingleChildScrollView( - child: SizedBox( - width: context.deviceWidth / 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTextField( - controller: nameController!, - label: 'Product Name', - keyboardType: TextInputType.text, - textInputAction: TextInputAction.next, - textCapitalization: TextCapitalization.words, - ), - const SpaceHeight(20.0), - CustomTextField( - controller: priceController!, - label: 'Price', - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, - onChanged: (value) { - priceValue = value.toIntegerFromText; - final int newValue = value.toIntegerFromText; - priceController!.text = newValue.currencyFormatRp; - priceController!.selection = TextSelection.fromPosition( - TextPosition(offset: priceController!.text.length)); - }, - ), - const SpaceHeight(20.0), - ImagePickerWidget( - label: 'Photo Product', - onChanged: (file) { - if (file == null) { - return; - } - imageFile = file; - }, - initialImageUrl: imageUrl, - ), - const SpaceHeight(20.0), - CustomTextField( - controller: stockController!, - label: 'Stock', - keyboardType: TextInputType.number, - ), - const SpaceHeight(20.0), - const Text( - "Category", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - const SpaceHeight(12.0), - BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - success: (categories) { - // Set the selected category if in edit mode and not already set - // if (isEditMode && - // selectCategory == null && - // widget.product?.category != null) { - // try { - // selectCategory = categories.firstWhere( - // (cat) => cat.id == widget.product!.category!.id, - // ); - // } catch (e) { - // // If no exact match found, leave selectCategory as null - // // This will show the hint text instead - // log("No matching category found for product category ID: ${widget.product!.category!.id}"); - // } - // } +// @override +// Widget build(BuildContext context) { +// return AlertDialog( +// title: Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// IconButton( +// onPressed: () => context.pop(), +// icon: const Icon(Icons.close), +// ), +// Text( +// isEditMode ? 'Edit Product' : 'Add Product', +// style: TextStyle(fontWeight: FontWeight.bold), +// ), +// const Spacer(), +// ], +// ), +// content: SingleChildScrollView( +// child: SizedBox( +// width: context.deviceWidth / 3, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// CustomTextField( +// controller: nameController!, +// label: 'Product Name', +// keyboardType: TextInputType.text, +// textInputAction: TextInputAction.next, +// textCapitalization: TextCapitalization.words, +// ), +// const SpaceHeight(20.0), +// CustomTextField( +// controller: priceController!, +// label: 'Price', +// keyboardType: TextInputType.number, +// textInputAction: TextInputAction.next, +// onChanged: (value) { +// priceValue = value.toIntegerFromText; +// final int newValue = value.toIntegerFromText; +// priceController!.text = newValue.currencyFormatRp; +// priceController!.selection = TextSelection.fromPosition( +// TextPosition(offset: priceController!.text.length)); +// }, +// ), +// const SpaceHeight(20.0), +// ImagePickerWidget( +// label: 'Photo Product', +// onChanged: (file) { +// if (file == null) { +// return; +// } +// imageFile = file; +// }, +// initialImageUrl: imageUrl, +// ), +// const SpaceHeight(20.0), +// CustomTextField( +// controller: stockController!, +// label: 'Stock', +// keyboardType: TextInputType.number, +// ), +// const SpaceHeight(20.0), +// const Text( +// "Category", +// style: TextStyle( +// fontSize: 14, +// fontWeight: FontWeight.w700, +// ), +// ), +// const SpaceHeight(12.0), +// BlocBuilder( +// builder: (context, state) { +// return state.maybeWhen( +// orElse: () { +// return const Center( +// child: CircularProgressIndicator(), +// ); +// }, +// success: (categories) { +// // Set the selected category if in edit mode and not already set +// // if (isEditMode && +// // selectCategory == null && +// // widget.product?.category != null) { +// // try { +// // selectCategory = categories.firstWhere( +// // (cat) => cat.id == widget.product!.category!.id, +// // ); +// // } catch (e) { +// // // If no exact match found, leave selectCategory as null +// // // This will show the hint text instead +// // log("No matching category found for product category ID: ${widget.product!.category!.id}"); +// // } +// // } - return DropdownButtonHideUnderline( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - child: DropdownButton( - value: selectCategory, - hint: const Text("Select Category"), - isExpanded: true, // Untuk mengisi lebar container - onChanged: (newValue) { - if (newValue != null) { - selectCategory = newValue; - setState(() {}); - log("selectCategory: ${selectCategory!.name}"); - } - }, - items: categories - .map>( - (CategoryModel category) { - return DropdownMenuItem( - value: category, - child: Text(category.name!), - ); - }).toList(), - ), - ), - ); - }, - ); - }, - ), - const SpaceHeight(12.0), - const Text( - "Print Type", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - //radio printer type - const SpaceHeight(12.0), - Row( - children: [ - Radio( - value: 'kitchen', - groupValue: printType, - onChanged: (value) { - setState(() { - printType = value; - }); - }, - ), - const Text('Kitchen'), - ], - ), - Row( - children: [ - Radio( - value: 'bar', - groupValue: printType, - onChanged: (value) { - setState(() { - printType = value; - }); - }, - ), - const Text('Bar'), - ], - ), - const SpaceHeight(20.0), - Row( - children: [ - Checkbox( - value: isBestSeller, - onChanged: (value) { - setState(() { - isBestSeller = value!; - }); - }, - ), - const Text('Favorite Product'), - ], - ), - const SpaceHeight(20.0), - const SpaceHeight(24.0), - if (isEditMode) - BlocConsumer( - listener: (context, state) { - state.maybeMap( - orElse: () {}, - success: (_) { - context - .read() - .add(const SyncProductEvent.syncProduct()); - context - .read() - .add(const GetProductsEvent.fetch()); - context.pop(true); +// return DropdownButtonHideUnderline( +// child: Container( +// decoration: BoxDecoration( +// border: Border.all(color: Colors.grey), +// borderRadius: BorderRadius.circular(12), +// ), +// padding: const EdgeInsets.symmetric( +// horizontal: 10, vertical: 5), +// child: DropdownButton( +// value: selectCategory, +// hint: const Text("Select Category"), +// isExpanded: true, // Untuk mengisi lebar container +// onChanged: (newValue) { +// if (newValue != null) { +// selectCategory = newValue; +// setState(() {}); +// log("selectCategory: ${selectCategory!.name}"); +// } +// }, +// items: categories +// .map>( +// (CategoryModel category) { +// return DropdownMenuItem( +// value: category, +// child: Text(category.name!), +// ); +// }).toList(), +// ), +// ), +// ); +// }, +// ); +// }, +// ), +// const SpaceHeight(12.0), +// const Text( +// "Print Type", +// style: TextStyle( +// fontSize: 14, +// fontWeight: FontWeight.w700, +// ), +// ), +// //radio printer type +// const SpaceHeight(12.0), +// Row( +// children: [ +// Radio( +// value: 'kitchen', +// groupValue: printType, +// onChanged: (value) { +// setState(() { +// printType = value; +// }); +// }, +// ), +// const Text('Kitchen'), +// ], +// ), +// Row( +// children: [ +// Radio( +// value: 'bar', +// groupValue: printType, +// onChanged: (value) { +// setState(() { +// printType = value; +// }); +// }, +// ), +// const Text('Bar'), +// ], +// ), +// const SpaceHeight(20.0), +// Row( +// children: [ +// Checkbox( +// value: isBestSeller, +// onChanged: (value) { +// setState(() { +// isBestSeller = value!; +// }); +// }, +// ), +// const Text('Favorite Product'), +// ], +// ), +// const SpaceHeight(20.0), +// const SpaceHeight(24.0), +// if (isEditMode) +// BlocConsumer( +// listener: (context, state) { +// state.maybeMap( +// orElse: () {}, +// success: (_) { +// context +// .read() +// .add(const SyncProductEvent.syncProduct()); +// context +// .read() +// .add(const GetProductsEvent.fetch()); +// context.pop(true); - const snackBar = SnackBar( - content: Text('Success Update Product'), - backgroundColor: AppColors.primary, - ); - ScaffoldMessenger.of(context).showSnackBar( - snackBar, - ); - }, - error: (message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: $message'), - backgroundColor: Colors.red, - ), - ); - }, - ); - }, - builder: (context, state) { - return state.maybeWhen( - orElse: () { - return Button.filled( - onPressed: () { - if (selectCategory == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please select a category'), - backgroundColor: Colors.red, - ), - ); - return; - } +// const snackBar = SnackBar( +// content: Text('Success Update Product'), +// backgroundColor: AppColors.primary, +// ); +// ScaffoldMessenger.of(context).showSnackBar( +// snackBar, +// ); +// }, +// error: (message) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text('Error: $message'), +// backgroundColor: Colors.red, +// ), +// ); +// }, +// ); +// }, +// builder: (context, state) { +// return state.maybeWhen( +// orElse: () { +// return Button.filled( +// onPressed: () { +// if (selectCategory == null) { +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar( +// content: Text('Please select a category'), +// backgroundColor: Colors.red, +// ), +// ); +// return; +// } - log("isBestSeller: $isBestSeller"); - final String name = nameController!.text; - final int stock = - stockController!.text.toIntegerFromText; +// log("isBestSeller: $isBestSeller"); +// final String name = nameController!.text; +// final int stock = +// stockController!.text.toIntegerFromText; - // final Product product = widget.product!.copyWith( - // name: name, - // price: priceValue.toString(), - // stock: stock, - // categoryId: selectCategory!.id!, - // isFavorite: isBestSeller ? 1 : 0, - // printerType: printType, - // ); +// // final Product product = widget.product!.copyWith( +// // name: name, +// // price: priceValue.toString(), +// // stock: stock, +// // categoryId: selectCategory!.id!, +// // isFavorite: isBestSeller ? 1 : 0, +// // printerType: printType, +// // ); - // context.read().add( - // UpdateProductEvent.updateProduct( - // product, imageFile)); - }, - label: 'Update Product', - ); - }, - loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - ); - }, - ) - else - BlocConsumer( - listener: (context, state) { - state.maybeMap( - orElse: () {}, - success: (_) { - context - .read() - .add(const SyncProductEvent.syncProduct()); - context - .read() - .add(const GetProductsEvent.fetch()); - context.pop(true); +// // context.read().add( +// // UpdateProductEvent.updateProduct( +// // product, imageFile)); +// }, +// label: 'Update Product', +// ); +// }, +// loading: () { +// return const Center( +// child: CircularProgressIndicator(), +// ); +// }, +// ); +// }, +// ) +// else +// BlocConsumer( +// listener: (context, state) { +// state.maybeMap( +// orElse: () {}, +// success: (_) { +// context +// .read() +// .add(const SyncProductEvent.syncProduct()); +// context +// .read() +// .add(const GetProductsEvent.fetch()); +// context.pop(true); - const snackBar = SnackBar( - content: Text('Success Add Product'), - backgroundColor: AppColors.primary, - ); - ScaffoldMessenger.of(context).showSnackBar( - snackBar, - ); - }, - ); - }, - builder: (context, state) { - return state.maybeWhen( - orElse: () { - return Button.filled( - onPressed: () { - if (selectCategory == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please select a category'), - backgroundColor: Colors.red, - ), - ); - return; - } +// const snackBar = SnackBar( +// content: Text('Success Add Product'), +// backgroundColor: AppColors.primary, +// ); +// ScaffoldMessenger.of(context).showSnackBar( +// snackBar, +// ); +// }, +// ); +// }, +// builder: (context, state) { +// return state.maybeWhen( +// orElse: () { +// return Button.filled( +// onPressed: () { +// if (selectCategory == null) { +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar( +// content: Text('Please select a category'), +// backgroundColor: Colors.red, +// ), +// ); +// return; +// } - log("isBestSeller: $isBestSeller"); - final String name = nameController!.text; +// log("isBestSeller: $isBestSeller"); +// final String name = nameController!.text; - final int stock = - stockController!.text.toIntegerFromText; - // final Product product = Product( - // name: name, - // price: priceValue.toString(), - // stock: stock, - // categoryId: selectCategory!.id!, - // isFavorite: isBestSeller ? 1 : 0, - // image: imageFile!.path, - // printerType: printType, - // ); - // context.read().add( - // AddProductEvent.addProduct( - // product, imageFile!)); - }, - label: 'Save Product', - ); - }, - loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - ); - }, - ), - const SpaceHeight(16.0), - ], - ), - ), - ), - ); - } -} +// final int stock = +// stockController!.text.toIntegerFromText; +// // final Product product = Product( +// // name: name, +// // price: priceValue.toString(), +// // stock: stock, +// // categoryId: selectCategory!.id!, +// // isFavorite: isBestSeller ? 1 : 0, +// // image: imageFile!.path, +// // printerType: printType, +// // ); +// // context.read().add( +// // AddProductEvent.addProduct( +// // product, imageFile!)); +// }, +// label: 'Save Product', +// ); +// }, +// loading: () { +// return const Center( +// child: CircularProgressIndicator(), +// ); +// }, +// ); +// }, +// ), +// const SpaceHeight(16.0), +// ], +// ), +// ), +// ), +// ); +// } +// } diff --git a/lib/presentation/setting/pages/sync_data_page.dart b/lib/presentation/setting/pages/sync_data_page.dart index d87f361..e5b690c 100644 --- a/lib/presentation/setting/pages/sync_data_page.dart +++ b/lib/presentation/setting/pages/sync_data_page.dart @@ -58,18 +58,18 @@ class _SyncDataPageState extends State { ); }, loaded: (productResponseModel) async { - await ProductLocalDatasource.instance - .deleteAllProducts(); - await ProductLocalDatasource.instance - .insertProducts( - productResponseModel.data!.products!, - ); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Sync Product Success2'), - backgroundColor: Colors.green, - ), - ); + // await ProductLocalDatasource.instance + // .deleteAllProducts(); + // await ProductLocalDatasource.instance + // .insertProducts( + // productResponseModel.data!.products!, + // ); + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Sync Product Success2'), + // backgroundColor: Colors.green, + // ), + // ); }, ); }, @@ -127,12 +127,12 @@ class _SyncDataPageState extends State { ); }, loaded: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Sync Order Success'), - backgroundColor: Colors.green, - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Sync Order Success'), + // backgroundColor: Colors.green, + // ), + // ); }, ); }, diff --git a/pubspec.lock b/pubspec.lock index 49df85f..999be1d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -366,6 +366,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + dropdown_search: + dependency: "direct main" + description: + name: dropdown_search + sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab" + url: "https://pub.dev" + source: hosted + version: "5.0.6" esc_pos_utils_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 33db1cb..d72b405 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: dio: ^5.8.0+1 awesome_dio_interceptor: ^1.3.0 another_flushbar: ^1.12.30 + dropdown_search: ^5.0.6 # imin_printer: ^0.6.10 dev_dependencies: