feat: create product

This commit is contained in:
efrilm
2025-08-05 19:20:45 +07:00
parent 935b6b9a5b
commit bb9aef55cf
24 changed files with 2750 additions and 848 deletions
@@ -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<Either<String, CategroyResponseModel>> getCategories() async {
final Dio dio = DioClient.instance;
Future<Either<String, CategoryResponseModel>> getCategories({
int page = 1,
int limit = 10,
bool isActive = true,
}) async {
final authData = await AuthLocalDataSource().getAuthData();
final Map<String, String> 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');
}
}
}
@@ -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<Either<String, FileResponseModel>> 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');
}
}
}
@@ -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<Either<String, AddProductResponseModel>> addProduct(
ProductRequestModel productRequestModel) async {
final authData = await AuthLocalDataSource().getAuthData();
final Map<String, String> 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<Either<String, AddProductResponseModel>> updateProduct(
ProductRequestModel productRequestModel) async {
final authData = await AuthLocalDataSource().getAuthData();
final Map<String, String> 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');
}
}
}
@@ -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<String, String> toMap() {
log("toMap: $isBestSeller");
final map = {
Map<String, dynamic> toMap() {
final map = <String, dynamic>{
'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;
@@ -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<String, dynamic> json) =>
AddProductResponseModel(
success: json["success"],
message: json["message"],
data: Product.fromMap(json["data"]),
);
Map<String, dynamic> toMap() => {
"success": success,
"message": message,
"data": data.toMap(),
};
}
@@ -1,91 +1,120 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
class CategroyResponseModel {
final String status;
final List<CategoryModel> 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<String, dynamic> toMap() {
return <String, dynamic>{
'status': status,
'data': data.map((x) => x.toMap()).toList(),
};
}
factory CategroyResponseModel.fromMap(Map<String, dynamic> map) {
return CategroyResponseModel(
status: map['status'] as String,
data: List<CategoryModel>.from(
(map['data']).map<CategoryModel>(
(x) => CategoryModel.fromMap(x as Map<String, dynamic>),
),
),
factory CategoryResponseModel.fromMap(Map<String, dynamic> map) {
return CategoryResponseModel(
success: map['success'] as bool,
data: CategoryData.fromMap(map['data'] as Map<String, dynamic>),
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<String, dynamic> toMap() {
return <String, dynamic>{
// '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<String, dynamic> 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<CategoryModel> 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<String, dynamic> map) {
return CategoryData(
categories: List<CategoryModel>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic>.from(map['metadata'] ?? {}),
createdAt: DateTime.parse(map['created_at'] as String),
updatedAt: DateTime.parse(map['updated_at'] as String),
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'organization_id': organizationId,
'name': name,
'description': description,
'business_type': businessType,
'metadata': metadata,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
}
@@ -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<String, dynamic> json) {
return FileResponseModel(
data: FileModel.fromJson(json['data']),
message: json['message'] as String,
success: json['success'] as bool,
);
}
Map<String, dynamic> toJson() => {
'data': data.toJson(),
'message': message,
'success': success,
};
factory FileResponseModel.fromMap(Map<String, dynamic> map) =>
FileResponseModel.fromJson(map);
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> map) =>
FileModel.fromJson(map);
Map<String, dynamic> 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)';
}
}