first commit
This commit is contained in:
382
lib/Screens/Products/Model/product_model.dart
Normal file
382
lib/Screens/Products/Model/product_model.dart
Normal file
@@ -0,0 +1,382 @@
|
||||
// --- Nested Helper Models ---
|
||||
|
||||
import 'package:mobile_pos/Screens/product%20racks/model/product_racks_model.dart';
|
||||
import 'package:mobile_pos/Screens/shelfs/model/shelf_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
|
||||
class Vat {
|
||||
final int? id;
|
||||
final num? rate; // Changed to num
|
||||
|
||||
Vat({this.id, this.rate});
|
||||
|
||||
factory Vat.fromJson(Map<String, dynamic> json) {
|
||||
return Vat(
|
||||
id: json['id'],
|
||||
rate: json['rate'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Unit {
|
||||
final int? id;
|
||||
final String? unitName;
|
||||
|
||||
Unit({this.id, this.unitName});
|
||||
|
||||
factory Unit.fromJson(Map<String, dynamic> json) {
|
||||
return Unit(
|
||||
id: json['id'],
|
||||
unitName: json['unitName'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Brand {
|
||||
final int? id;
|
||||
final String? brandName;
|
||||
|
||||
Brand({this.id, this.brandName});
|
||||
|
||||
factory Brand.fromJson(Map<String, dynamic> json) {
|
||||
return Brand(
|
||||
id: json['id'],
|
||||
brandName: json['brandName'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Category {
|
||||
final int? id;
|
||||
final String? categoryName;
|
||||
|
||||
Category({this.id, this.categoryName});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) {
|
||||
return Category(
|
||||
id: json['id'],
|
||||
categoryName: json['categoryName'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProductModel {
|
||||
final int? id;
|
||||
final String? name;
|
||||
|
||||
ProductModel({this.id, this.name});
|
||||
|
||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductModel(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WarrantyGuaranteeInfo {
|
||||
final String? warrantyDuration;
|
||||
final String? warrantyUnit;
|
||||
final String? guaranteeDuration;
|
||||
final String? guaranteeUnit;
|
||||
|
||||
WarrantyGuaranteeInfo({
|
||||
this.warrantyDuration,
|
||||
this.warrantyUnit,
|
||||
this.guaranteeDuration,
|
||||
this.guaranteeUnit,
|
||||
});
|
||||
|
||||
factory WarrantyGuaranteeInfo.fromJson(Map<String, dynamic> json) {
|
||||
return WarrantyGuaranteeInfo(
|
||||
warrantyDuration: json['warranty_duration'],
|
||||
warrantyUnit: json['warranty_unit'],
|
||||
guaranteeDuration: json['guarantee_duration'],
|
||||
guaranteeUnit: json['guarantee_unit'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a specific stock/batch of a product, potentially with variants.
|
||||
class Stock {
|
||||
final int? id;
|
||||
final int? businessId;
|
||||
final int? branchId;
|
||||
final int? warehouseId;
|
||||
final int? productId;
|
||||
final String? batchNo;
|
||||
final num? productStock; // Changed to num
|
||||
final num? productPurchasePrice;
|
||||
final num? profitPercent; // Changed to num
|
||||
final num? productSalePrice;
|
||||
final num? productWholeSalePrice;
|
||||
final num? productDealerPrice;
|
||||
final String? serialNumbers;
|
||||
// Variation data is an array of maps
|
||||
final List<Map<String, dynamic>>? variationData;
|
||||
final String? variantName;
|
||||
final String? mfgDate;
|
||||
final String? expireDate;
|
||||
final String? createdAt;
|
||||
final String? updatedAt;
|
||||
final String? deletedAt;
|
||||
final Product? product;
|
||||
final WarehouseData? warehouse;
|
||||
|
||||
Stock({
|
||||
this.id,
|
||||
this.businessId,
|
||||
this.branchId,
|
||||
this.warehouseId,
|
||||
this.productId,
|
||||
this.batchNo,
|
||||
this.productStock,
|
||||
this.productPurchasePrice,
|
||||
this.profitPercent,
|
||||
this.productSalePrice,
|
||||
this.productWholeSalePrice,
|
||||
this.productDealerPrice,
|
||||
this.serialNumbers,
|
||||
this.variationData,
|
||||
this.variantName,
|
||||
this.mfgDate,
|
||||
this.expireDate,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.product,
|
||||
this.warehouse,
|
||||
});
|
||||
|
||||
factory Stock.fromJson(Map<String, dynamic> json) {
|
||||
return Stock(
|
||||
id: json['id'],
|
||||
businessId: json['business_id'],
|
||||
branchId: json['branch_id'],
|
||||
warehouseId: json['warehouse_id'],
|
||||
productId: json['product_id'],
|
||||
batchNo: json['batch_no'],
|
||||
productStock: json['productStock'],
|
||||
productPurchasePrice: json['productPurchasePrice'],
|
||||
profitPercent: json['profit_percent'],
|
||||
productSalePrice: json['productSalePrice'],
|
||||
productWholeSalePrice: json['productWholeSalePrice'],
|
||||
productDealerPrice: json['productDealerPrice'],
|
||||
serialNumbers: json['serial_numbers'],
|
||||
variationData: (json['variation_data'] as List?)?.cast<Map<String, dynamic>>(),
|
||||
variantName: json['variant_name'],
|
||||
mfgDate: json['mfg_date'],
|
||||
expireDate: json['expire_date'],
|
||||
createdAt: json['created_at'],
|
||||
updatedAt: json['updated_at'],
|
||||
deletedAt: json['deleted_at'],
|
||||
product: json['product'] != null ? Product.fromJson(json['product']) : null,
|
||||
warehouse: json['warehouse'] != null ? WarehouseData.fromJson(json['warehouse']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a component product within a 'combo' product.
|
||||
class ComboProductComponent {
|
||||
final int? id;
|
||||
final int? productId;
|
||||
final int? stockId;
|
||||
final num? purchasePrice;
|
||||
final num? quantity; // Changed to num
|
||||
final Stock? stock;
|
||||
|
||||
ComboProductComponent({
|
||||
this.id,
|
||||
this.productId,
|
||||
this.stockId,
|
||||
this.purchasePrice,
|
||||
this.quantity,
|
||||
this.stock,
|
||||
});
|
||||
|
||||
factory ComboProductComponent.fromJson(Map<String, dynamic> json) {
|
||||
return ComboProductComponent(
|
||||
id: json['id'],
|
||||
productId: json['product_id'],
|
||||
stockId: json['stock_id'],
|
||||
purchasePrice: json['purchase_price'],
|
||||
quantity: json['quantity'],
|
||||
stock: json['stock'] != null ? Stock.fromJson(json['stock']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Product Model ---
|
||||
|
||||
/// Represents a single product entity.
|
||||
class Product {
|
||||
final int? id;
|
||||
final String? productName;
|
||||
final int? businessId;
|
||||
final int? rackId;
|
||||
final int? shelfId;
|
||||
final int? unitId;
|
||||
final int? brandId;
|
||||
final int? categoryId;
|
||||
final String? productCode;
|
||||
final WarrantyGuaranteeInfo? warrantyGuaranteeInfo;
|
||||
// variation_ids is a List<String> or null
|
||||
final List<String>? variationIds;
|
||||
final String? productPicture;
|
||||
final String? productType;
|
||||
final num? productDealerPrice;
|
||||
final num? totalLossProfit;
|
||||
final num? productPurchasePrice;
|
||||
final num? totalSaleAmount;
|
||||
final num? productSalePrice;
|
||||
final num? saleCount;
|
||||
final num? purchaseCount;
|
||||
final num? productWholeSalePrice;
|
||||
final num? productStock; // Changed to num
|
||||
final String? expireDate;
|
||||
final num? alertQty; // Changed to num
|
||||
final num? profitPercent; // Changed to num
|
||||
final num? vatAmount;
|
||||
final String? vatType;
|
||||
final String? size;
|
||||
final String? type;
|
||||
final String? color;
|
||||
final String? weight;
|
||||
final String? capacity;
|
||||
final String? productManufacturer;
|
||||
final dynamic meta; // Use 'dynamic' for unstructured JSON
|
||||
final String? createdAt;
|
||||
final String? updatedAt;
|
||||
final int? vatId;
|
||||
final int? modelId;
|
||||
final int? warehouseId;
|
||||
final num? stocksSumProductStock; // Changed to num
|
||||
|
||||
// Relationships (Nested Objects/Lists)
|
||||
final Unit? unit;
|
||||
final Vat? vat;
|
||||
final Brand? brand;
|
||||
final Category? category;
|
||||
final ProductModel? productModel;
|
||||
final List<Stock>? stocks;
|
||||
final RackData? rack;
|
||||
final ShelfData? shelf;
|
||||
final List<ComboProductComponent>? comboProducts;
|
||||
|
||||
Product({
|
||||
this.id,
|
||||
this.productName,
|
||||
this.businessId,
|
||||
this.rackId,
|
||||
this.shelfId,
|
||||
this.unitId,
|
||||
this.brandId,
|
||||
this.categoryId,
|
||||
this.productCode,
|
||||
this.totalLossProfit,
|
||||
this.warrantyGuaranteeInfo,
|
||||
this.variationIds,
|
||||
this.productPicture,
|
||||
this.productType,
|
||||
this.productDealerPrice,
|
||||
this.saleCount,
|
||||
this.purchaseCount,
|
||||
this.productPurchasePrice,
|
||||
this.productSalePrice,
|
||||
this.productWholeSalePrice,
|
||||
this.totalSaleAmount,
|
||||
this.productStock,
|
||||
this.expireDate,
|
||||
this.alertQty,
|
||||
this.profitPercent,
|
||||
this.vatAmount,
|
||||
this.vatType,
|
||||
this.size,
|
||||
this.type,
|
||||
this.color,
|
||||
this.weight,
|
||||
this.capacity,
|
||||
this.productManufacturer,
|
||||
this.meta,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.vatId,
|
||||
this.modelId,
|
||||
this.warehouseId,
|
||||
this.stocksSumProductStock,
|
||||
this.unit,
|
||||
this.vat,
|
||||
this.brand,
|
||||
this.category,
|
||||
this.productModel,
|
||||
this.stocks,
|
||||
this.comboProducts,
|
||||
this.rack,
|
||||
this.shelf,
|
||||
});
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) {
|
||||
// Helper function to safely map lists, returning null if the source is null
|
||||
List<T>? _mapList<T>(List? list, T Function(Map<String, dynamic>) fromJson) {
|
||||
return list?.map((i) => fromJson(i as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
return Product(
|
||||
id: json['id'],
|
||||
productName: json['productName'],
|
||||
businessId: json['business_id'],
|
||||
rackId: json['rack_id'],
|
||||
shelfId: json['shelf_id'],
|
||||
unitId: json['unit_id'],
|
||||
brandId: json['brand_id'],
|
||||
categoryId: json['category_id'],
|
||||
productCode: json['productCode'],
|
||||
warrantyGuaranteeInfo: json['warranty_guarantee_info'] != null
|
||||
? WarrantyGuaranteeInfo.fromJson(json['warranty_guarantee_info'])
|
||||
: null,
|
||||
variationIds: (json['variation_ids'] as List?)?.cast<String>(),
|
||||
productPicture: json['productPicture'],
|
||||
totalLossProfit: json['total_profit_loss'],
|
||||
productType: json['product_type'],
|
||||
productDealerPrice: json['productDealerPrice'],
|
||||
totalSaleAmount: json['total_sale_amount'],
|
||||
saleCount: json['sale_details_sum_quantities'],
|
||||
purchaseCount: json['purchase_details_sum_quantities'],
|
||||
productPurchasePrice: json['productPurchasePrice'],
|
||||
productSalePrice: json['productSalePrice'],
|
||||
productWholeSalePrice: json['productWholeSalePrice'],
|
||||
productStock: json['productStock'],
|
||||
expireDate: json['expire_date'],
|
||||
alertQty: json['alert_qty'],
|
||||
profitPercent: json['profit_percent'],
|
||||
vatAmount: json['vat_amount'],
|
||||
vatType: json['vat_type'],
|
||||
size: json['size'],
|
||||
type: json['type'],
|
||||
color: json['color'],
|
||||
weight: json['weight'],
|
||||
capacity: json['capacity'],
|
||||
productManufacturer: json['productManufacturer'],
|
||||
meta: json['meta'],
|
||||
createdAt: json['created_at'],
|
||||
updatedAt: json['updated_at'],
|
||||
vatId: json['vat_id'],
|
||||
modelId: json['model_id'],
|
||||
warehouseId: json['warehouse_id'],
|
||||
stocksSumProductStock: json['stocks_sum_product_stock'],
|
||||
|
||||
// Nested Relationships
|
||||
unit: json['unit'] != null ? Unit.fromJson(json['unit']) : null,
|
||||
shelf: json['shelf'] != null ? ShelfData.fromJson(json['shelf']) : null,
|
||||
rack: json['rack'] != null ? RackData.fromJson(json['rack']) : null,
|
||||
vat: json['vat'] != null ? Vat.fromJson(json['vat']) : null,
|
||||
brand: json['brand'] != null ? Brand.fromJson(json['brand']) : null,
|
||||
category: json['category'] != null ? Category.fromJson(json['category']) : null,
|
||||
productModel: json['product_model'] != null ? ProductModel.fromJson(json['product_model']) : null,
|
||||
|
||||
// Lists of Nested Objects
|
||||
stocks: _mapList<Stock>(json['stocks'] as List?, Stock.fromJson),
|
||||
comboProducts: _mapList<ComboProductComponent>(json['combo_products'] as List?, ComboProductComponent.fromJson),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/Screens/Products/Model/product_total_stock_model.dart
Normal file
18
lib/Screens/Products/Model/product_total_stock_model.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
|
||||
class ProductListResponse {
|
||||
final double totalStockValue;
|
||||
final List<Product> products;
|
||||
|
||||
ProductListResponse({
|
||||
required this.totalStockValue,
|
||||
required this.products,
|
||||
});
|
||||
|
||||
factory ProductListResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ProductListResponse(
|
||||
totalStockValue: (json['total_stock_value'] as num).toDouble(),
|
||||
products: (json['data'] as List).map((item) => Product.fromJson(item)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
lib/Screens/Products/Providers/product_provider.dart
Normal file
11
lib/Screens/Products/Providers/product_provider.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_total_stock_model.dart';
|
||||
|
||||
import '../Repo/product_repo.dart';
|
||||
|
||||
ProductRepo productRepo = ProductRepo();
|
||||
final productListProvider = FutureProvider<ProductListResponse>((ref) async {
|
||||
final response = await productRepo.fetchProducts();
|
||||
return response;
|
||||
});
|
||||
448
lib/Screens/Products/Repo/product_repo.dart
Normal file
448
lib/Screens/Products/Repo/product_repo.dart
Normal file
@@ -0,0 +1,448 @@
|
||||
//ignore_for_file: file_names, unused_element, unused_local_variable
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_total_stock_model.dart';
|
||||
import 'package:mobile_pos/service/check_user_role_permission_provider.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../constant.dart';
|
||||
import '../../../core/constant_variables/local_data_saving_keys.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../../Purchase/Repo/purchase_repo.dart';
|
||||
import '../Model/product_model.dart';
|
||||
import '../add product/modle/create_product_model.dart';
|
||||
|
||||
class ProductRepo {
|
||||
// ==============================================================================
|
||||
// NEW CREATE PRODUCT FUNCTION
|
||||
// ==============================================================================
|
||||
Future<bool> createProduct({required CreateProductModel data, required BuildContext context, required WidgetRef ref}) async {
|
||||
return _submitProductData(data: data, isUpdate: false, context: context, ref: ref);
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// NEW UPDATE PRODUCT FUNCTION
|
||||
// ==============================================================================
|
||||
Future<bool> updateProduct({required CreateProductModel data, required BuildContext context, required WidgetRef ref}) async {
|
||||
return _submitProductData(data: data, isUpdate: true, context: context, ref: ref);
|
||||
}
|
||||
|
||||
/// Shared Logic for Create and Update to avoid code duplication
|
||||
Future<bool> _submitProductData({required CreateProductModel data, required bool isUpdate, required BuildContext context, required WidgetRef ref}) async {
|
||||
EasyLoading.show(status: isUpdate ? 'Updating Product...' : 'Creating Product...');
|
||||
|
||||
final url = Uri.parse(isUpdate ? '${APIConfig.url}/products/${data.productId}' : '${APIConfig.url}/products');
|
||||
|
||||
var request = http.MultipartRequest('POST', url);
|
||||
|
||||
request.headers.addAll({
|
||||
'Accept': 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
});
|
||||
|
||||
// Helper to safely add simple string fields
|
||||
void addField(String key, dynamic value) {
|
||||
if (value != null && value.toString().isNotEmpty && value.toString() != 'null') {
|
||||
request.fields[key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 1. Standard Fields ---
|
||||
if (isUpdate) addField('_method', 'put');
|
||||
|
||||
addField('productName', data.name);
|
||||
addField('category_id', data.categoryId);
|
||||
addField('unit_id', data.unitId);
|
||||
addField('productCode', data.productCode);
|
||||
addField('brand_id', data.brandId);
|
||||
addField('model_id', data.modelId);
|
||||
addField('rack_id', data.rackId);
|
||||
addField('shelf_id', data.shelfId);
|
||||
addField('alert_qty', data.alertQty);
|
||||
|
||||
// Serial logic (1 or 0)
|
||||
// addField('has_serial', (data.hasSerial == '1' || data.hasSerial == 'true') ? '1' : '0');
|
||||
|
||||
addField('product_type', data.productType); // single, variant, combo
|
||||
addField('vat_type', data.vatType);
|
||||
addField('vat_id', data.vatId);
|
||||
// Optional: vat_amount if backend calculates it or needs it
|
||||
if (data.vatAmount != null) addField('vat_amount', data.vatAmount);
|
||||
|
||||
// Extra info
|
||||
addField('productManufacturer', data.productManufacturer);
|
||||
addField('productDiscount', data.productDiscount);
|
||||
|
||||
// --- 2. Complex Fields (JSON Encoded) ---
|
||||
|
||||
// A. STOCKS
|
||||
// This handles Single (1 item in list) and Variant (multiple items in list)
|
||||
if (data.stocks != null && data.stocks!.isNotEmpty) {
|
||||
// Convert list of StockDataModel to List of Maps
|
||||
List<Map<String, dynamic>> stockListJson = data.stocks!.map((stock) => stock.toJson()).toList();
|
||||
// Encode to JSON String
|
||||
request.fields['stocks'] = jsonEncode(stockListJson);
|
||||
}
|
||||
|
||||
// B. VARIATION IDs (Only for variant type)
|
||||
if (data.productType?.toLowerCase() == 'variant' && (data.variationIds?.isNotEmpty ?? false)) {
|
||||
request.fields['variation_ids'] = jsonEncode(data.variationIds);
|
||||
}
|
||||
|
||||
// C. COMBO PRODUCTS (Only for combo type)
|
||||
if (data.productType?.toLowerCase() == 'combo' && (data.comboProducts?.isNotEmpty ?? false)) {
|
||||
request.fields['combo_products'] = jsonEncode(data.comboProducts);
|
||||
addField('profit_percent', data.comboProfitPercent);
|
||||
addField('productSalePrice', data.comboProductSalePrice);
|
||||
}
|
||||
|
||||
// D. WARRANTY & GUARANTEE
|
||||
Map<String, String> warrantyInfo = {};
|
||||
if (data.warrantyDuration != null && data.warrantyDuration!.isNotEmpty) {
|
||||
warrantyInfo['warranty_duration'] = data.warrantyDuration!;
|
||||
warrantyInfo['warranty_unit'] = data.warrantyPeriod ?? 'days';
|
||||
}
|
||||
if (data.guaranteeDuration != null && data.guaranteeDuration!.isNotEmpty) {
|
||||
warrantyInfo['guarantee_duration'] = data.guaranteeDuration!;
|
||||
warrantyInfo['guarantee_unit'] = data.guaranteePeriod ?? 'days';
|
||||
}
|
||||
|
||||
if (warrantyInfo.isNotEmpty) {
|
||||
request.fields['warranty_guarantee_info'] = jsonEncode(warrantyInfo);
|
||||
}
|
||||
|
||||
// --- 3. File Upload ---
|
||||
if (data.image != null) {
|
||||
request.files.add(await http.MultipartFile.fromPath(
|
||||
'productPicture',
|
||||
data.image!.path,
|
||||
filename: data.image!.path.split('/').last,
|
||||
));
|
||||
}
|
||||
|
||||
// --- Debugging Logs ---
|
||||
print('URL: $url');
|
||||
print('--- Fields ---');
|
||||
|
||||
request.fields.forEach((key, value) {
|
||||
print('$key: $value');
|
||||
});
|
||||
print('--- Fields ---');
|
||||
print(request.fields);
|
||||
|
||||
// --- 4. Execute ---
|
||||
try {
|
||||
// var response = await request.send();
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), ref: ref, context: context);
|
||||
print('Product image: ${data.image?.path}');
|
||||
final response = await customHttpClient.uploadFile(
|
||||
url: url,
|
||||
file: data.image,
|
||||
fileFieldName: 'productPicture',
|
||||
fields: request.fields,
|
||||
);
|
||||
var responseData = await http.Response.fromStream(response);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
print("Response Status: ${response.statusCode}");
|
||||
print("Response Body: ${responseData.body}");
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showSuccess(body['message'] ?? (isUpdate ? 'Updated successfully!' : 'Created successfully!'));
|
||||
return true;
|
||||
} catch (e) {
|
||||
// If JSON parsing fails but status is 200
|
||||
EasyLoading.showSuccess(isUpdate ? 'Product updated!' : 'Product created!');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showError(body['message'] ?? 'Failed to process product');
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Failed with status: ${response.statusCode}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Network Error: ${e.toString()}');
|
||||
print(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> generateProductCode() async {
|
||||
final uri = Uri.parse('${APIConfig.url}/product/generate-code');
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
try {
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final jsonResponse = json.decode(response.body);
|
||||
return jsonResponse['data'].toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Product>> fetchAllProducts() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/products');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final partyList = parsedData['data'] as List<dynamic>;
|
||||
return partyList.map((category) => Product.fromJson(category)).toList();
|
||||
// Parse into Party objects
|
||||
} else {
|
||||
throw Exception('Failed to fetch Products');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProductListResponse> fetchProducts() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/products');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return ProductListResponse.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch products');
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Product Details
|
||||
Future<Product> fetchProductDetails({required String productID}) async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
final url = Uri.parse('${APIConfig.url}/products/$productID');
|
||||
|
||||
try {
|
||||
var response = await clientGet.get(url: url);
|
||||
EasyLoading.dismiss();
|
||||
print(response.statusCode);
|
||||
print(response.body);
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
return Product.fromJson(jsonData['data']);
|
||||
} else {
|
||||
var data = jsonDecode(response.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Failed to fetch details');
|
||||
throw Exception(data['message'] ?? 'Failed to fetch details');
|
||||
}
|
||||
} catch (e) {
|
||||
// Hide loading indicator and show error
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
throw Exception('Error: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteProduct({
|
||||
required String id,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
final String apiUrl = '${APIConfig.url}/products/$id';
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(
|
||||
ref: ref,
|
||||
context: context,
|
||||
client: http.Client(),
|
||||
);
|
||||
|
||||
final response = await customHttpClient.delete(
|
||||
url: Uri.parse(apiUrl),
|
||||
permission: Permit.productsDelete.value,
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
// 👇 Print full response info
|
||||
print('Delete Product Response:');
|
||||
print('Status Code: ${response.statusCode}');
|
||||
print('Body: ${response.body}');
|
||||
print('Headers: ${response.headers}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Product deleted successfully')),
|
||||
);
|
||||
|
||||
ref.refresh(productProvider);
|
||||
} else {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
final errorMessage = parsedData['error'].toString().replaceFirst('Exception: ', '');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(errorMessage),
|
||||
backgroundColor: kMainColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('rrrr');
|
||||
EasyLoading.dismiss();
|
||||
print('Exception during product delete: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addStock({required String id, required String qty}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/stocks');
|
||||
String token = await getAuthToken() ?? '';
|
||||
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token,
|
||||
};
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
"stock_id": id,
|
||||
"productStock": qty,
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: requestBody);
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
final data = jsonDecode(response.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Unknown error');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateVariation({required CartProductModelPurchase data}) async {
|
||||
EasyLoading.show(status: 'Updating Product...');
|
||||
final url = Uri.parse('${APIConfig.url}/stocks/${data.variantName}');
|
||||
var request = http.MultipartRequest('POST', url);
|
||||
|
||||
request.headers.addAll({
|
||||
'Accept': 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
});
|
||||
|
||||
void addField(String key, dynamic value) {
|
||||
if (value != null && value.toString().isNotEmpty && value.toString() != 'null') {
|
||||
request.fields[key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Add standard fields
|
||||
addField('_method', 'put');
|
||||
addField('batch_no', data.batchNumber);
|
||||
addField('productStock', data.quantities);
|
||||
addField('productPurchasePrice', data.productPurchasePrice);
|
||||
addField('profit_percent', data.profitPercent);
|
||||
addField('productSalePrice', data.productSalePrice);
|
||||
addField('productWholeSalePrice', data.productWholeSalePrice);
|
||||
addField('productDealerPrice', data.productDealerPrice);
|
||||
addField('mfg_date', data.mfgDate);
|
||||
addField('expire_date', data.expireDate);
|
||||
|
||||
print('--- Product Data Fields ---');
|
||||
print('Total fields: ${request.fields.length}');
|
||||
print(data.mfgDate);
|
||||
request.fields.forEach((key, value) {
|
||||
print('$key: $value');
|
||||
});
|
||||
|
||||
try {
|
||||
var response = await request.send();
|
||||
var responseData = await http.Response.fromStream(response);
|
||||
|
||||
print('Response Status Code: ${response.statusCode}');
|
||||
print('Response Body: ${responseData.body}');
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showSuccess(body['message'] ?? 'Product update successfully!');
|
||||
return true;
|
||||
} catch (e) {
|
||||
EasyLoading.showSuccess('Product update successfully!');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showError(body['message'] ?? 'Failed to update product');
|
||||
print('Error Response: ${responseData.body}');
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Failed to update product. Status: ${response.statusCode}');
|
||||
print('Error Response (non-JSON): ${responseData.body}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Network Error: ${e.toString()}');
|
||||
print('Network Error: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteStock({required String id}) async {
|
||||
EasyLoading.show(status: 'Processing');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String token = prefs.getString(LocalDataBaseSavingKey.tokenKey) ?? '';
|
||||
final url = Uri.parse('${APIConfig.url}/stocks/$id');
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
try {
|
||||
var response = await http.delete(
|
||||
url,
|
||||
headers: headers,
|
||||
);
|
||||
EasyLoading.dismiss();
|
||||
print(response.statusCode);
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
var data = jsonDecode(response.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Failed to delete');
|
||||
print(data['message']);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
print(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
lib/Screens/Products/Repo/unit_repo.dart
Normal file
154
lib/Screens/Products/Repo/unit_repo.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
// ignore_for_file: file_names, unused_element, unused_local_variable
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../../product_unit/model/unit_model.dart';
|
||||
import '../../product_unit/provider/product_unit_provider.dart';
|
||||
|
||||
class UnitsRepo {
|
||||
Future<List<Unit>> fetchAllUnits() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/units');
|
||||
|
||||
try {
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final categoryList = parsedData['data'] as List<dynamic>;
|
||||
return categoryList.map((unit) => Unit.fromJson(unit)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch units: ${response.statusCode}');
|
||||
}
|
||||
} catch (error) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addUnit({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required String name,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/units');
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: {
|
||||
'unitName': name,
|
||||
},
|
||||
// addContentTypeInHeader: true,
|
||||
);
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successful!')));
|
||||
var data1 = ref.refresh(unitsProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Unit creation failed: ${parsedData['message']}')));
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<num?> addUnitForBulk({
|
||||
required String name,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/units');
|
||||
|
||||
try {
|
||||
var responseData = await http.post(uri, headers: {
|
||||
"Accept": 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
}, body: {
|
||||
'unitName': name,
|
||||
});
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
return parsedData['data']['id'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
///_______Edit_Add_________________________________________
|
||||
Future<void> editUnit({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num id,
|
||||
required String name,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/units/$id');
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: {
|
||||
'unitName': name,
|
||||
'_method': 'put',
|
||||
},
|
||||
);
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('update successful!')));
|
||||
var data1 = ref.refresh(unitsProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Unit creation failed: ${parsedData['message']}')));
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
}
|
||||
}
|
||||
|
||||
///_________delete_unit________________________
|
||||
Future<bool> deleteUnit({required BuildContext context, required num unitId, required WidgetRef ref}) async {
|
||||
final String apiUrl = '${APIConfig.url}/units/$unitId'; // Replace with your API URL
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
|
||||
final response = await customHttpClient.delete(
|
||||
url: Uri.parse(apiUrl),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = json.decode(response.body);
|
||||
final String message = responseData['message'];
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete unit.')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred.')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
217
lib/Screens/Products/Widgets/acnoo_multiple_select_dropdown.dart
Normal file
217
lib/Screens/Products/Widgets/acnoo_multiple_select_dropdown.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'dart:ui';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Widgets/selected_button.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class AcnooMultiSelectDropdown<T> extends StatefulWidget {
|
||||
AcnooMultiSelectDropdown({
|
||||
super.key,
|
||||
this.decoration,
|
||||
this.menuItemStyleData,
|
||||
this.buttonStyleData,
|
||||
this.iconStyleData,
|
||||
this.dropdownStyleData,
|
||||
required this.items,
|
||||
this.values,
|
||||
this.onChanged,
|
||||
required this.labelText,
|
||||
}) : assert(
|
||||
items.isEmpty ||
|
||||
values == null ||
|
||||
items.where((item) {
|
||||
return values.contains(item.value);
|
||||
}).length ==
|
||||
values.length,
|
||||
"There should be exactly one item with [AcnooMultiSelectDropdown]'s value in the items list. "
|
||||
'Either zero or 2 or more [MultiSelectDropdownMenuItem]s were detected with the same value',
|
||||
);
|
||||
|
||||
final List<MultiSelectDropdownMenuItem<T?>> items;
|
||||
final List<T?>? values;
|
||||
final void Function(List<T>? values)? onChanged;
|
||||
|
||||
final InputDecoration? decoration;
|
||||
final MenuItemStyleData? menuItemStyleData;
|
||||
final ButtonStyleData? buttonStyleData;
|
||||
final IconStyleData? iconStyleData;
|
||||
final DropdownStyleData? dropdownStyleData;
|
||||
|
||||
final String labelText;
|
||||
|
||||
@override
|
||||
State<AcnooMultiSelectDropdown<T>> createState() => _AcnooMultiSelectDropdownState<T>();
|
||||
}
|
||||
|
||||
class _AcnooMultiSelectDropdownState<T> extends State<AcnooMultiSelectDropdown<T>> {
|
||||
bool isOpen = false;
|
||||
void listenMenuChange(bool value) {
|
||||
setState(() {
|
||||
isOpen = value;
|
||||
if (!value) {
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
late List<MultiSelectDropdownMenuItem<T?>> selectedItems;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedItems = widget.items.where((element) => widget.values?.contains(element.value) ?? false).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return DropdownButtonFormField2<T>(
|
||||
decoration: (widget.decoration ?? const InputDecoration()).copyWith(
|
||||
labelText: widget.labelText,
|
||||
hintText: '',
|
||||
),
|
||||
menuItemStyleData: widget.menuItemStyleData ?? const MenuItemStyleData(),
|
||||
buttonStyleData: widget.buttonStyleData ?? const ButtonStyleData(),
|
||||
iconStyleData: widget.iconStyleData ?? const IconStyleData(),
|
||||
dropdownStyleData: widget.dropdownStyleData ?? const DropdownStyleData(),
|
||||
onMenuStateChange: listenMenuChange,
|
||||
customButton: _buildCustomButton(context),
|
||||
items: widget.items.map((item) {
|
||||
return DropdownMenuItem<T>(
|
||||
value: item.value,
|
||||
enabled: false,
|
||||
child: _buildMenuItem(context, item, _theme),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (_) {},
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- CHANGE IS HERE ---------------- //
|
||||
Widget _buildCustomButton(BuildContext context) {
|
||||
const _itemPadding = EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
);
|
||||
if (selectedItems.isEmpty) {
|
||||
final _iconWidget = widget.iconStyleData?.icon ?? Icon(Icons.keyboard_arrow_down_outlined);
|
||||
return Padding(
|
||||
padding: _itemPadding,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.decoration?.hintText ?? lang.S.of(context).selectItems,
|
||||
style: widget.decoration?.hintStyle,
|
||||
),
|
||||
_iconWidget,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ScrollConfiguration(
|
||||
behavior: const ScrollBehavior().copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.touch,
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: selectedItems.reversed.map((item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SelectedItemButton(
|
||||
padding: _itemPadding,
|
||||
labelText: item.labelText,
|
||||
onTap: () {
|
||||
// 1. Remove item from local state
|
||||
setState(() {
|
||||
selectedItems.remove(item);
|
||||
});
|
||||
|
||||
// 2. Trigger the onChanged callback immediately
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// ----------------------------------------------- //
|
||||
|
||||
Widget _buildMenuItem(
|
||||
BuildContext context,
|
||||
MultiSelectDropdownMenuItem<T?> item,
|
||||
ThemeData _theme,
|
||||
) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, itemState) {
|
||||
final _isSelected = selectedItems.contains(item);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_isSelected ? selectedItems.remove(item) : selectedItems.add(item);
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
setState(() {});
|
||||
itemState(() {});
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(item.labelText),
|
||||
),
|
||||
if (_isSelected)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: kMainColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSelectDropdownMenuItem<T> {
|
||||
MultiSelectDropdownMenuItem({
|
||||
required this.labelText,
|
||||
this.value,
|
||||
});
|
||||
final String labelText;
|
||||
final T? value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MultiSelectDropdownMenuItem<T> &&
|
||||
runtimeType == other.runtimeType &&
|
||||
labelText == other.labelText &&
|
||||
value == other.value;
|
||||
|
||||
@override
|
||||
int get hashCode => labelText.hashCode ^ value.hashCode;
|
||||
}
|
||||
228
lib/Screens/Products/Widgets/dropdown_styles.dart
Normal file
228
lib/Screens/Products/Widgets/dropdown_styles.dart
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class AcnooDropdownStyle {
|
||||
AcnooDropdownStyle(this.context);
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
// Theme
|
||||
ThemeData get _theme => Theme.of(context);
|
||||
bool get _isDark => _theme.brightness == Brightness.dark;
|
||||
|
||||
// Button Style
|
||||
ButtonStyleData get buttonStyle => const ButtonStyleData(width: 0);
|
||||
|
||||
// Dropdown Style
|
||||
AcnooDropdownStyleData get dropdownStyle {
|
||||
return AcnooDropdownStyleData(
|
||||
maxHeight: 300,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Icon Style
|
||||
AcnooDropdownIconData get iconStyle {
|
||||
return AcnooDropdownIconData(
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: _isDark ? Colors.white : kMainColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Menu Style
|
||||
AcnooDropdownMenuItemStyleData get menuItemStyle {
|
||||
return AcnooDropdownMenuItemStyleData(
|
||||
overlayColor: WidgetStateProperty.all<Color>(
|
||||
kMainColor.withValues(alpha: 0.25),
|
||||
),
|
||||
selectedMenuItemBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withValues(alpha: 0.125),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MenuItemStyleData get multiSelectMenuItemStyle {
|
||||
return MenuItemStyleData(
|
||||
overlayColor: WidgetStateProperty.all<Color>(
|
||||
kMainColor.withValues(alpha: 0.25),
|
||||
),
|
||||
selectedMenuItemBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withValues(alpha: 0.125),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
child,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: kMainColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Text Style
|
||||
TextStyle? get textStyle => _theme.textTheme.bodyLarge;
|
||||
|
||||
/*
|
||||
DropdownMenuItem<T> firstItem<T>({
|
||||
required String title,
|
||||
required String actionTitle,
|
||||
void Function()? onTap,
|
||||
T? value,
|
||||
bool enabled = false,
|
||||
}) {
|
||||
return DropdownMenuItem(
|
||||
value: value,
|
||||
enabled: enabled,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AcnooTextStyle.kSubtitleText.copyWith(
|
||||
fontSize: 16,
|
||||
color: AcnooAppColors.k03,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: actionTitle,
|
||||
recognizer: TapGestureRecognizer()..onTap = onTap,
|
||||
),
|
||||
style: AcnooTextStyle.kSubtitleText.copyWith(
|
||||
fontSize: 14,
|
||||
color: AcnooAppColors.kPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownStyleData extends DropdownStyleData {
|
||||
const AcnooDropdownStyleData({
|
||||
super.maxHeight,
|
||||
super.width,
|
||||
super.padding,
|
||||
super.scrollPadding,
|
||||
super.decoration,
|
||||
super.elevation,
|
||||
super.direction,
|
||||
super.offset,
|
||||
super.isOverButton,
|
||||
super.useSafeArea,
|
||||
super.isFullScreen,
|
||||
super.useRootNavigator,
|
||||
super.scrollbarTheme,
|
||||
super.openInterval,
|
||||
});
|
||||
|
||||
AcnooDropdownStyleData copyWith({
|
||||
double? maxHeight,
|
||||
double? width,
|
||||
EdgeInsetsGeometry? padding,
|
||||
EdgeInsetsGeometry? scrollPadding,
|
||||
BoxDecoration? decoration,
|
||||
int? elevation,
|
||||
DropdownDirection? direction,
|
||||
Offset? offset,
|
||||
bool? isOverButton,
|
||||
bool? useSafeArea,
|
||||
bool? isFullScreen,
|
||||
bool? useRootNavigator,
|
||||
ScrollbarThemeData? scrollbarTheme,
|
||||
Interval? openInterval,
|
||||
}) {
|
||||
return AcnooDropdownStyleData(
|
||||
maxHeight: maxHeight ?? this.maxHeight,
|
||||
width: width ?? this.width,
|
||||
padding: padding ?? this.padding,
|
||||
scrollPadding: scrollPadding ?? this.scrollPadding,
|
||||
decoration: decoration ?? this.decoration,
|
||||
elevation: elevation ?? this.elevation,
|
||||
direction: direction ?? this.direction,
|
||||
offset: offset ?? this.offset,
|
||||
isOverButton: isOverButton ?? this.isOverButton,
|
||||
useSafeArea: useSafeArea ?? this.useSafeArea,
|
||||
isFullScreen: isFullScreen ?? this.useRootNavigator,
|
||||
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
|
||||
scrollbarTheme: scrollbarTheme ?? this.scrollbarTheme,
|
||||
openInterval: openInterval ?? this.openInterval,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownIconData extends IconStyleData {
|
||||
const AcnooDropdownIconData({
|
||||
super.icon,
|
||||
super.iconDisabledColor,
|
||||
super.iconEnabledColor,
|
||||
super.iconSize,
|
||||
super.openMenuIcon,
|
||||
});
|
||||
|
||||
AcnooDropdownIconData copyWith({
|
||||
Widget? icon,
|
||||
Color? iconDisabledColor,
|
||||
Color? iconEnabledColor,
|
||||
double? iconSize,
|
||||
Widget? openMenuIcon,
|
||||
}) {
|
||||
return AcnooDropdownIconData(
|
||||
icon: icon ?? this.icon,
|
||||
iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor,
|
||||
iconEnabledColor: iconEnabledColor ?? this.iconEnabledColor,
|
||||
iconSize: iconSize ?? this.iconSize,
|
||||
openMenuIcon: openMenuIcon ?? this.openMenuIcon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownMenuItemStyleData extends MenuItemStyleData {
|
||||
const AcnooDropdownMenuItemStyleData({
|
||||
super.customHeights,
|
||||
super.height,
|
||||
super.overlayColor,
|
||||
super.padding,
|
||||
super.selectedMenuItemBuilder,
|
||||
});
|
||||
|
||||
AcnooDropdownMenuItemStyleData copyWith({
|
||||
List<double>? customHeights,
|
||||
double? height,
|
||||
Color? overlayColor,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Widget Function(BuildContext, Widget)? selectedMenuItemBuilder,
|
||||
}) {
|
||||
return AcnooDropdownMenuItemStyleData(
|
||||
customHeights: customHeights ?? this.customHeights,
|
||||
height: height ?? this.height,
|
||||
overlayColor: overlayColor != null ? WidgetStateProperty.all<Color?>(overlayColor) : this.overlayColor,
|
||||
selectedMenuItemBuilder: selectedMenuItemBuilder ?? this.selectedMenuItemBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/Screens/Products/Widgets/selected_button.dart
Normal file
58
lib/Screens/Products/Widgets/selected_button.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class SelectedItemButton extends StatelessWidget {
|
||||
const SelectedItemButton({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
this.padding,
|
||||
this.onTap,
|
||||
this.showCloseButton = true,
|
||||
});
|
||||
final String labelText;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final void Function()? onTap;
|
||||
final bool showCloseButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: padding ??
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: kDarkWhite,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: labelText,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
if (showCloseButton)
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 12,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(color: _theme.colorScheme.onPrimary),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/Screens/Products/Widgets/text_field_label_wrappers.dart
Normal file
37
lib/Screens/Products/Widgets/text_field_label_wrappers.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// 🐦 Flutter imports:
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class TextFieldLabelWrapper extends StatelessWidget {
|
||||
const TextFieldLabelWrapper({
|
||||
super.key,
|
||||
this.labelText,
|
||||
this.label,
|
||||
this.labelStyle,
|
||||
required this.inputField,
|
||||
});
|
||||
final String? labelText;
|
||||
final Widget? label;
|
||||
final TextStyle? labelStyle;
|
||||
final Widget inputField;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Label
|
||||
if (label == null)
|
||||
Text(
|
||||
labelText ?? lang.S.of(context).enterLabelText,
|
||||
style: labelStyle ?? _theme.inputDecorationTheme.floatingLabelStyle,
|
||||
)
|
||||
else
|
||||
label!,
|
||||
const SizedBox(height: 8),
|
||||
inputField,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
724
lib/Screens/Products/add product/add_edit_comboItem.dart
Normal file
724
lib/Screens/Products/add product/add_edit_comboItem.dart
Normal file
@@ -0,0 +1,724 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import '../../../Provider/product_provider.dart';
|
||||
import '../../../constant.dart';
|
||||
import 'combo_product_form.dart';
|
||||
|
||||
class AddOrEditComboItem extends ConsumerStatefulWidget {
|
||||
final ComboItem? existingItem;
|
||||
final Function(ComboItem) onSubmit;
|
||||
|
||||
const AddOrEditComboItem({
|
||||
super.key,
|
||||
this.existingItem,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<AddOrEditComboItem> createState() => _AddOrEditComboItemPopupState();
|
||||
}
|
||||
|
||||
class _AddOrEditComboItemPopupState extends ConsumerState<AddOrEditComboItem> {
|
||||
Product? selectedProduct;
|
||||
Stock? selectedStock;
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final TextEditingController qtyController = TextEditingController();
|
||||
final TextEditingController unitController = TextEditingController();
|
||||
final TextEditingController priceController = TextEditingController();
|
||||
final TextEditingController totalController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.existingItem != null) {
|
||||
final item = widget.existingItem!;
|
||||
selectedProduct = item.product;
|
||||
selectedStock = item.stockData;
|
||||
|
||||
if (item.product.productType == 'variant' && selectedStock != null) {
|
||||
searchController.text = "${item.product.productName} - ${selectedStock?.variantName}";
|
||||
} else {
|
||||
searchController.text = item.product.productName ?? '';
|
||||
}
|
||||
|
||||
qtyController.text = item.quantity.toString();
|
||||
unitController.text = item.product.unit?.unitName ?? 'Pcs';
|
||||
|
||||
priceController.text = (item.manualPurchasePrice ?? selectedStock?.productPurchasePrice ?? 0).toString();
|
||||
|
||||
_calculateTotal();
|
||||
}
|
||||
|
||||
// if (widget.existingItem != null) {
|
||||
// // Load existing data for Edit Mode
|
||||
// final item = widget.existingItem!;
|
||||
// selectedProduct = item.product;
|
||||
// selectedStock = item.stockData;
|
||||
// searchController.text = item.product.productName ?? '';
|
||||
// qtyController.text = item.quantity.toString();
|
||||
// unitController.text = item.product.unit?.unitName ?? 'Pcs';
|
||||
// priceController.text = item.purchasePrice.toString();
|
||||
// _calculateTotal();
|
||||
// } else {
|
||||
// // Add Mode Defaults
|
||||
// qtyController.text = '1';
|
||||
// unitController.text = 'Pcs';
|
||||
// }
|
||||
}
|
||||
|
||||
void _calculateTotal() {
|
||||
double qty = double.tryParse(qtyController.text) ?? 0;
|
||||
double price = double.tryParse(priceController.text) ?? 0;
|
||||
totalController.text = (qty * price).toStringAsFixed(2);
|
||||
}
|
||||
|
||||
late var _searchController = TextEditingController();
|
||||
// Product? selectedCustomer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productListAsync = ref.watch(productProvider);
|
||||
final _theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.existingItem == null ? _lang.addProduct : _lang.editProduct,
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 22,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1.0),
|
||||
child: Divider(height: 1, thickness: 1, color: kBottomBorder),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.existingItem == null) ...[
|
||||
// --------------use typehead---------------------
|
||||
productListAsync.when(
|
||||
data: (products) {
|
||||
// Filter out combos
|
||||
final filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
|
||||
return TypeAheadField<Map<String, dynamic>>(
|
||||
emptyBuilder: (context) => Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(_lang.noItemFound),
|
||||
),
|
||||
builder: (context, controller, focusNode) {
|
||||
_searchController = controller;
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
hintText: selectedProduct != null ? selectedProduct?.productName : _lang.searchProduct,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
selectedProduct = null;
|
||||
selectedStock = null;
|
||||
setState(() {});
|
||||
},
|
||||
icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
suggestionsCallback: (pattern) {
|
||||
final query = pattern.toLowerCase().trim();
|
||||
final List<Map<String, dynamic>> suggestions = [];
|
||||
|
||||
for (var product in filteredProducts) {
|
||||
// Skip combo products (already filtered above)
|
||||
if (product.productType != 'variant') {
|
||||
final productName = (product.productName ?? '').toLowerCase();
|
||||
if (query.isEmpty || productName.contains(query)) {
|
||||
suggestions.add({'type': 'single', 'product': product});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Variant product
|
||||
bool headerAdded = false;
|
||||
final parentName = (product.productName ?? '').toLowerCase();
|
||||
|
||||
for (var s in product.stocks ?? []) {
|
||||
final variantName = (s.variantName ?? '').toLowerCase();
|
||||
|
||||
// Combine parent name + variant name for searching
|
||||
final combinedName = '$parentName $variantName';
|
||||
|
||||
if (query.isEmpty || combinedName.contains(query)) {
|
||||
if (!headerAdded) {
|
||||
suggestions.add({'type': 'header', 'product': product});
|
||||
headerAdded = true;
|
||||
}
|
||||
suggestions.add({
|
||||
'type': 'variant',
|
||||
'product': product,
|
||||
'stock': s,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
},
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final query = pattern.toLowerCase().trim();
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// if (product.productType != 'variant') {
|
||||
// // Single product is selectable
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // Variant parent is only a header
|
||||
// bool headerAdded = false;
|
||||
//
|
||||
// // Check if parent name matches
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// // Check variant names
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// final variantName = (s.variantName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || variantName.contains(query)) {
|
||||
// if (!headerAdded) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
itemBuilder: (context, suggestion) {
|
||||
final type = suggestion['type'] as String;
|
||||
|
||||
if (type == 'header') {
|
||||
final p = suggestion['product'] as Product;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// Just close the suggestion box without selecting anything
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: Icon(Icons.circle, color: Colors.black, size: 10),
|
||||
title: Text(
|
||||
p.productName ?? '',
|
||||
style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (type == 'variant') {
|
||||
final product = suggestion['product'] as Product;
|
||||
final stock = suggestion['stock'] as Stock;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
subtitle: Text(
|
||||
'${_lang.stock}: ${stock.productStock}, ${_lang.price}: $currency${stock.productPurchasePrice}, ${_lang.batch}: ${stock.batchNo}'),
|
||||
);
|
||||
}
|
||||
|
||||
// single product
|
||||
final product = suggestion['product'] as Product;
|
||||
return ListTile(
|
||||
title: Text(product.productName ?? ''),
|
||||
subtitle: Text(
|
||||
'${_lang.stock}: ${product.stocksSumProductStock ?? 0}, ${_lang.price}: $currency${product.productPurchasePrice}'),
|
||||
);
|
||||
},
|
||||
onSelected: (suggestion) {
|
||||
final type = suggestion['type'] as String;
|
||||
|
||||
if (type == 'variant' || type == 'single') {
|
||||
final product = suggestion['product'] as Product;
|
||||
|
||||
setState(() {
|
||||
selectedProduct = product;
|
||||
|
||||
if (type == 'variant') {
|
||||
selectedStock = suggestion['stock'] as Stock;
|
||||
} else {
|
||||
selectedStock = product.stocks?.isNotEmpty == true ? product.stocks!.first : null;
|
||||
}
|
||||
|
||||
_searchController.text = type == 'variant'
|
||||
? "${product.productName} - ${selectedStock?.variantName}"
|
||||
: product.productName ?? '';
|
||||
|
||||
unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
priceController.text = (selectedStock?.productPurchasePrice ?? 0).toStringAsFixed(2);
|
||||
|
||||
_calculateTotal();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => LinearProgressIndicator(),
|
||||
error: (e, _) => Text("Error: $e"),
|
||||
),
|
||||
// productListAsync.when(
|
||||
// data: (products) {
|
||||
// final List<Product> filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
//
|
||||
// return TypeAheadField<Map<String, dynamic>>(
|
||||
// emptyBuilder: (context) => Padding(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// child: Text("No item found"),
|
||||
// ),
|
||||
// builder: (context, controller, focusNode) {
|
||||
// _searchController = controller;
|
||||
// return TextField(
|
||||
// controller: controller,
|
||||
// focusNode: focusNode,
|
||||
// decoration: InputDecoration(
|
||||
// prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
// hintText: selectedProduct != null ? selectedProduct?.productName : 'Search product',
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () {
|
||||
// controller.clear();
|
||||
// selectedProduct = null;
|
||||
// selectedStock = null;
|
||||
// setState(() {});
|
||||
// },
|
||||
// icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final query = pattern.toLowerCase().trim();
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (product.productType != 'variant') {
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// bool headerAdded = false;
|
||||
//
|
||||
// if (query.isEmpty) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
//
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (productName.contains(query)) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// final variantName = (s.variantName ?? '').toLowerCase();
|
||||
//
|
||||
// if (variantName.contains(query)) {
|
||||
// if (!headerAdded) {
|
||||
// // Only add header once
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
// itemBuilder: (context, suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// if (type == 'header') {
|
||||
// final p = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.circle, color: Colors.black, size: 10),
|
||||
// title: Text(
|
||||
// p.productName ?? '',
|
||||
// style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// // header is not selectable, so we make it visually disabled
|
||||
// enabled: false,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// final stock = suggestion['stock'] as Stock;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
// title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
// subtitle: Text('Stock: ${stock.productStock}, Price: $currency${stock.productPurchasePrice}, Batch: ${stock.batchNo}'),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // single product
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// title: Text(product.productName ?? ''),
|
||||
// subtitle: Text('Stock: ${product.stocksSumProductStock ?? 0}, Price: $currency${product.productPurchasePrice}'),
|
||||
// );
|
||||
// },
|
||||
// onSelected: (suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// // Only allow single or variant selection
|
||||
// if (type == 'single' || type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// setState(() {
|
||||
// selectedProduct = product;
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// selectedStock = suggestion['stock'] as Stock;
|
||||
// } else {
|
||||
// selectedStock = product.stocks?.isNotEmpty == true ? product.stocks!.first : null;
|
||||
// }
|
||||
//
|
||||
// // Update search field
|
||||
// _searchController.text = type == 'variant' ? "${product.productName} - ${selectedStock?.variantName}" : product.productName ?? '';
|
||||
//
|
||||
// // Update unit field
|
||||
// unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
//
|
||||
// // Update price field
|
||||
// priceController.text = (selectedStock?.productPurchasePrice ?? 0).toStringAsFixed(2);
|
||||
//
|
||||
// // Recalculate total
|
||||
// _calculateTotal();
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// loading: () => LinearProgressIndicator(),
|
||||
// error: (e, _) => Text("Error: $e"),
|
||||
// ),
|
||||
// --------------use typehead---------------------
|
||||
] else ...[
|
||||
TextFormField(
|
||||
controller: searchController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.product,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// --------previous code-----------------
|
||||
// if (widget.existingItem == null) ...[
|
||||
// // --------------use typehead---------------------
|
||||
// productListAsync.when(
|
||||
// data: (products) {
|
||||
// // Filter out combo products
|
||||
// final filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
//
|
||||
// return TypeAheadField<Map<String, dynamic>>(
|
||||
// builder: (context, controller, focusNode) {
|
||||
// return TextField(
|
||||
// controller: _searchController,
|
||||
// focusNode: focusNode,
|
||||
// decoration: InputDecoration(
|
||||
// prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
// hintText: selectedProduct != null ? selectedProduct?.productName : 'Search product',
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () {
|
||||
// _searchController.clear();
|
||||
// selectedProduct = null;
|
||||
// setState(() {});
|
||||
// },
|
||||
// icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// if (product.productType == 'variant') {
|
||||
// // Show parent product as a header if it matches the search
|
||||
// if ((product.productName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// }
|
||||
//
|
||||
// // Show variant stocks
|
||||
// for (var stock in product.stocks ?? []) {
|
||||
// if ((stock.variantName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'variant', 'product': product, 'stock': stock});
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Single product
|
||||
// if ((product.productName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
// itemBuilder: (context, suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// if (type == 'header') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(
|
||||
// Icons.circle,
|
||||
// color: Colors.black,
|
||||
// size: 10,
|
||||
// ),
|
||||
// title: Text(
|
||||
// product.productName ?? '',
|
||||
// style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// );
|
||||
// } else if (type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// final stock = suggestion['stock'] as Stock;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
// title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
// subtitle: Text('Stock: ${stock.productStock}, Price: $currency${stock.productPurchasePrice}, Batch: ${stock.batchNo}'),
|
||||
// );
|
||||
// } else {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// title: Text(product.productName ?? ''),
|
||||
// subtitle: Text('Stock: ${product.stocksSumProductStock ?? 0}, Price: $currency${product.productPurchasePrice}'),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// onSelected: (suggestion) {
|
||||
// setState(() {
|
||||
// final type = suggestion['type'] as String;
|
||||
// final product = suggestion['product'] as Product;
|
||||
//
|
||||
// selectedProduct = product;
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// selectedStock = suggestion['stock'] as Stock;
|
||||
// } else {
|
||||
// selectedStock = product.stocks != null && product.stocks!.isNotEmpty ? product.stocks!.first : null;
|
||||
// }
|
||||
//
|
||||
// _searchController.text = type == 'variant' ? "${product.productName} - ${selectedStock?.variantName}" : product.productName ?? '';
|
||||
//
|
||||
// unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
// priceController.text = (selectedStock?.productPurchasePrice ?? 0).toString();
|
||||
// _calculateTotal();
|
||||
// });
|
||||
//
|
||||
// FocusScope.of(context).unfocus();
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// loading: () => const Center(child: LinearProgressIndicator()),
|
||||
// error: (e, stack) => Text('Error: $e'),
|
||||
// ),
|
||||
// // --------------use typehead---------------------
|
||||
// ] else ...[
|
||||
// TextFormField(
|
||||
// controller: searchController,
|
||||
// readOnly: true,
|
||||
// decoration: const InputDecoration(
|
||||
// labelText: 'Product',
|
||||
// border: OutlineInputBorder(),
|
||||
// filled: true,
|
||||
// fillColor: Color(0xFFF5F5F5),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
SizedBox(height: 20),
|
||||
// --- Row 1: Quantity & Units ---
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: qtyController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.quantity,
|
||||
hintText: 'Ex: 1',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => _calculateTotal(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: unitController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.units,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Row 2: Purchase Price & Total ---
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: priceController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.purchasePrice,
|
||||
hintText: 'Ex: 20',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => _calculateTotal(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: totalController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.total,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: DAppColors.kWarning),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
_lang.cancel,
|
||||
style: TextStyle(
|
||||
color: DAppColors.kWarning,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
// minimumSize: Size.fromHeight(48),
|
||||
backgroundColor: const Color(0xFFB71C1C), // Red color
|
||||
),
|
||||
onPressed: () {
|
||||
if (selectedProduct != null && selectedStock != null) {
|
||||
final newItem = ComboItem(
|
||||
product: selectedProduct!,
|
||||
stockData: selectedStock!,
|
||||
quantity: int.tryParse(qtyController.text) ?? 1,
|
||||
manualPurchasePrice: double.tryParse(priceController.text),
|
||||
);
|
||||
widget.onSubmit(newItem);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Please select a product")));
|
||||
}
|
||||
},
|
||||
child: Text(_lang.save, style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1225
lib/Screens/Products/add product/add_product.dart
Normal file
1225
lib/Screens/Products/add product/add_product.dart
Normal file
File diff suppressed because it is too large
Load Diff
318
lib/Screens/Products/add product/combo_product_form.dart
Normal file
318
lib/Screens/Products/add product/combo_product_form.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/invoice_constant.dart' hide kMainColor;
|
||||
import '../../../Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'add_edit_comboItem.dart';
|
||||
import 'modle/create_product_model.dart';
|
||||
|
||||
// Updated Helper Model to support manual price override
|
||||
class ComboItem {
|
||||
final Product product;
|
||||
final Stock stockData;
|
||||
int quantity;
|
||||
double? manualPurchasePrice; // Added this field
|
||||
|
||||
ComboItem({
|
||||
required this.product,
|
||||
required this.stockData,
|
||||
this.quantity = 1,
|
||||
this.manualPurchasePrice,
|
||||
});
|
||||
|
||||
// Use manual price if set, otherwise stock price
|
||||
double get purchasePrice => manualPurchasePrice ?? (stockData.productPurchasePrice ?? 0).toDouble();
|
||||
double get totalAmount => purchasePrice * quantity;
|
||||
}
|
||||
|
||||
class ComboProductForm extends ConsumerStatefulWidget {
|
||||
final TextEditingController profitController;
|
||||
final TextEditingController saleController;
|
||||
final TextEditingController purchasePriceController;
|
||||
final List<ComboProductModel>? initialComboList;
|
||||
final Function(List<ComboProductModel>) onComboListChanged;
|
||||
|
||||
const ComboProductForm({
|
||||
super.key,
|
||||
required this.profitController,
|
||||
required this.saleController,
|
||||
required this.purchasePriceController,
|
||||
this.initialComboList,
|
||||
required this.onComboListChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ComboProductForm> createState() => _ComboProductFormState();
|
||||
}
|
||||
|
||||
class _ComboProductFormState extends ConsumerState<ComboProductForm> {
|
||||
List<ComboItem> selectedComboItems = [];
|
||||
bool _isDataLoaded = false;
|
||||
|
||||
// --- Calculation Logic (Same as before) ---
|
||||
void _calculateValues({String? source}) {
|
||||
double totalPurchase = 0;
|
||||
for (var item in selectedComboItems) {
|
||||
totalPurchase += item.totalAmount;
|
||||
}
|
||||
|
||||
if (widget.purchasePriceController.text != totalPurchase.toStringAsFixed(2)) {
|
||||
widget.purchasePriceController.text = totalPurchase.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
double purchase = totalPurchase;
|
||||
double profit = double.tryParse(widget.profitController.text) ?? 0;
|
||||
double sale = double.tryParse(widget.saleController.text) ?? 0;
|
||||
|
||||
if (source == 'margin') {
|
||||
sale = purchase + (purchase * profit / 100);
|
||||
widget.saleController.text = sale.toStringAsFixed(2);
|
||||
} else if (source == 'sale') {
|
||||
if (purchase > 0) {
|
||||
profit = ((sale - purchase) / purchase) * 100;
|
||||
widget.profitController.text = profit.toStringAsFixed(2);
|
||||
}
|
||||
} else {
|
||||
sale = purchase + (purchase * profit / 100);
|
||||
widget.saleController.text = sale.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
List<ComboProductModel> finalApiList = selectedComboItems.map((item) {
|
||||
return ComboProductModel(
|
||||
stockId: item.stockData.id.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
purchasePrice: item.purchasePrice.toString(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
widget.onComboListChanged(finalApiList);
|
||||
}
|
||||
|
||||
// --- Open the Popup for Add or Edit ---
|
||||
void openProductForm({ComboItem? item, int? index}) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddOrEditComboItem(
|
||||
existingItem: item,
|
||||
onSubmit: (newItem) {
|
||||
setState(() {
|
||||
if (index != null) {
|
||||
// Edit Mode: Replace item
|
||||
selectedComboItems[index] = newItem;
|
||||
} else {
|
||||
// Add Mode: Check duplicate or add new
|
||||
bool exists = false;
|
||||
for (int i = 0; i < selectedComboItems.length; i++) {
|
||||
if (selectedComboItems[i].stockData.id == newItem.stockData.id) {
|
||||
// If same product exists, just update that entry
|
||||
selectedComboItems[i] = newItem;
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) selectedComboItems.add(newItem);
|
||||
}
|
||||
_calculateValues(source: 'item_updated');
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productListAsync = ref.watch(productProvider);
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
// Load Initial Data Logic
|
||||
productListAsync.whenData((products) {
|
||||
if (!_isDataLoaded && widget.initialComboList != null && widget.initialComboList!.isNotEmpty) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
List<ComboItem> tempLoadedItems = [];
|
||||
for (var initialItem in widget.initialComboList!) {
|
||||
for (var product in products) {
|
||||
if (product.stocks != null) {
|
||||
try {
|
||||
var matchingStock =
|
||||
product.stocks!.firstWhere((s) => s.id.toString() == initialItem.stockId.toString());
|
||||
tempLoadedItems.add(ComboItem(
|
||||
product: product,
|
||||
stockData: matchingStock,
|
||||
quantity: int.tryParse(initialItem.quantity.toString()) ?? 1,
|
||||
manualPurchasePrice: double.tryParse(initialItem.purchasePrice.toString()),
|
||||
));
|
||||
break;
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
selectedComboItems = tempLoadedItems;
|
||||
_isDataLoaded = true;
|
||||
});
|
||||
_calculateValues(source: 'init');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Add Product Button
|
||||
ElevatedButton(
|
||||
onPressed: () => openProductForm(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kMainColor50, // Light reddish background
|
||||
minimumSize: Size(131, 36),
|
||||
elevation: 0,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
),
|
||||
child: Text(
|
||||
"+ ${l.S.of(context).addProduct}",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. List of Items (Matching Screenshot 1)
|
||||
if (selectedComboItems.isNotEmpty)
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: selectedComboItems.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
height: 1,
|
||||
color: kLineColor,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = selectedComboItems[index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: 0),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.product.productType == 'single'
|
||||
? item.product.productName ?? 'n/a'
|
||||
: ('${item.product.productName ?? ''} (${item.product.stocks?[index].variantName ?? 'n/a'})'),
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${l.S.of(context).qty}: ${item.quantity}',
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${l.S.of(context).code} : ${item.product.productCode ?? 'n/a'}, ${l.S.of(context).batchNo}: ${item.stockData.batchNo ?? 'n/a'}',
|
||||
style: _theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${item.totalAmount ?? 'n/a'}',
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
iconColor: kPeraColor,
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
openProductForm(item: item, index: index);
|
||||
} else if (value == 'delete') {
|
||||
setState(() {
|
||||
selectedComboItems.removeAt(index);
|
||||
_calculateValues(source: 'item_removed');
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(value: 'edit', child: Text(l.S.of(context).edit)),
|
||||
PopupMenuItem(
|
||||
value: 'delete', child: Text(l.S.of(context).delete, style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (selectedComboItems.isNotEmpty)
|
||||
const Divider(
|
||||
height: 1,
|
||||
color: kLineColor,
|
||||
),
|
||||
SizedBox(height: 13),
|
||||
// 3. Footer: Net Total, Profit, Sale Price
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("${l.S.of(context).netTotalAmount}:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text("\$${widget.purchasePriceController.text}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.profitController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l.S.of(context).profitMargin} (%)',
|
||||
hintText: 'Ex: 25%',
|
||||
border: OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
onChanged: (value) => _calculateValues(source: 'margin'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.saleController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).defaultSellingPrice,
|
||||
hintText: 'Ex: 150',
|
||||
border: OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
onChanged: (value) => _calculateValues(source: 'sale'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
150
lib/Screens/Products/add product/modle/create_product_model.dart
Normal file
150
lib/Screens/Products/add product/modle/create_product_model.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:io';
|
||||
|
||||
// Enum for clearer logic in UI
|
||||
enum ProductType { single, variant, combo }
|
||||
|
||||
// --- 1. Combo Product Model ---
|
||||
class ComboProductModel {
|
||||
ComboProductModel({
|
||||
this.stockId,
|
||||
this.quantity,
|
||||
this.purchasePrice,
|
||||
});
|
||||
|
||||
String? stockId;
|
||||
String? quantity;
|
||||
String? purchasePrice;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'stock_id': stockId,
|
||||
'quantity': quantity,
|
||||
'purchase_price': purchasePrice,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. Stock Data Model (Existing) ---
|
||||
class StockDataModel {
|
||||
StockDataModel({
|
||||
this.stockId,
|
||||
this.batchNo,
|
||||
this.warehouseId,
|
||||
this.productStock,
|
||||
this.exclusivePrice,
|
||||
this.inclusivePrice,
|
||||
this.profitPercent,
|
||||
this.productSalePrice,
|
||||
this.productWholeSalePrice,
|
||||
this.productDealerPrice,
|
||||
this.mfgDate,
|
||||
this.expireDate,
|
||||
this.serialNumbers,
|
||||
this.variantName,
|
||||
this.variationData,
|
||||
this.subStock,
|
||||
});
|
||||
|
||||
String? stockId;
|
||||
String? batchNo;
|
||||
String? warehouseId;
|
||||
String? productStock;
|
||||
String? exclusivePrice;
|
||||
String? inclusivePrice;
|
||||
String? profitPercent;
|
||||
String? productSalePrice;
|
||||
String? productWholeSalePrice;
|
||||
String? productDealerPrice;
|
||||
String? mfgDate;
|
||||
String? expireDate;
|
||||
List<String>? serialNumbers;
|
||||
bool? subStock;
|
||||
String? variantName;
|
||||
List<Map<String, dynamic>>? variationData;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'stock_id': stockId,
|
||||
'batch_no': batchNo,
|
||||
'warehouse_id': warehouseId,
|
||||
'productStock': productStock,
|
||||
'exclusive_price': exclusivePrice,
|
||||
'inclusive_price': inclusivePrice,
|
||||
'profit_percent': profitPercent == 'Infinity' ? '0' : profitPercent,
|
||||
'productSalePrice': productSalePrice,
|
||||
'productWholeSalePrice': productWholeSalePrice,
|
||||
'productDealerPrice': productDealerPrice,
|
||||
'mfg_date': mfgDate,
|
||||
'expire_date': expireDate,
|
||||
'serial_numbers': serialNumbers,
|
||||
'variant_name': variantName,
|
||||
'variation_data': variationData,
|
||||
};
|
||||
data.removeWhere((key, value) => value == null || value.toString().isEmpty || value == 'null');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Main Create Product Model ---
|
||||
class CreateProductModel {
|
||||
CreateProductModel({
|
||||
this.productId,
|
||||
this.name,
|
||||
this.categoryId,
|
||||
this.brandId,
|
||||
this.productCode,
|
||||
this.modelId,
|
||||
this.rackId,
|
||||
this.shelfId,
|
||||
this.alertQty,
|
||||
this.unitId,
|
||||
this.vatId,
|
||||
this.vatType,
|
||||
this.vatAmount,
|
||||
this.image,
|
||||
this.productType,
|
||||
this.stocks,
|
||||
this.comboProducts,
|
||||
this.variationIds,
|
||||
this.warrantyDuration,
|
||||
this.warrantyPeriod,
|
||||
this.guaranteeDuration,
|
||||
this.guaranteePeriod,
|
||||
this.productManufacturer,
|
||||
this.productDiscount,
|
||||
this.comboProfitPercent,
|
||||
this.comboProductSalePrice,
|
||||
});
|
||||
|
||||
String? productId;
|
||||
String? name;
|
||||
String? categoryId;
|
||||
String? brandId;
|
||||
String? productCode;
|
||||
String? modelId;
|
||||
String? rackId;
|
||||
String? shelfId;
|
||||
String? alertQty;
|
||||
String? unitId;
|
||||
String? vatId;
|
||||
String? vatType;
|
||||
String? vatAmount;
|
||||
File? image;
|
||||
String? productType;
|
||||
String? comboProfitPercent;
|
||||
String? comboProductSalePrice;
|
||||
|
||||
// Lists
|
||||
List<StockDataModel>? stocks;
|
||||
List<ComboProductModel>? comboProducts;
|
||||
List<String?>? variationIds;
|
||||
|
||||
String? productManufacturer;
|
||||
String? productDiscount;
|
||||
|
||||
String? warrantyDuration;
|
||||
String? warrantyPeriod;
|
||||
String? guaranteeDuration;
|
||||
String? guaranteePeriod;
|
||||
}
|
||||
373
lib/Screens/Products/add product/single_product_form.dart
Normal file
373
lib/Screens/Products/add product/single_product_form.dart
Normal file
@@ -0,0 +1,373 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/model/get_product_setting_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../../constant.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../warehouse/warehouse_provider/warehouse_provider.dart';
|
||||
|
||||
class SingleProductForm extends ConsumerWidget {
|
||||
const SingleProductForm({
|
||||
super.key,
|
||||
required this.snapShot,
|
||||
required this.batchController,
|
||||
required this.stockController,
|
||||
required this.purchaseExController,
|
||||
required this.purchaseIncController,
|
||||
required this.profitController,
|
||||
required this.saleController,
|
||||
required this.wholesaleController,
|
||||
required this.dealerController,
|
||||
required this.mfgDateController,
|
||||
required this.expDateController,
|
||||
this.selectedWarehouse,
|
||||
required this.onWarehouseChanged,
|
||||
required this.onPriceChanged,
|
||||
required this.onMfgDateSelected,
|
||||
required this.onExpDateSelected,
|
||||
});
|
||||
|
||||
final GetProductSettingModel snapShot;
|
||||
|
||||
// Controllers passed from Parent
|
||||
final TextEditingController batchController;
|
||||
final TextEditingController stockController;
|
||||
final TextEditingController purchaseExController;
|
||||
final TextEditingController purchaseIncController;
|
||||
final TextEditingController profitController;
|
||||
final TextEditingController saleController;
|
||||
final TextEditingController wholesaleController;
|
||||
final TextEditingController dealerController;
|
||||
final TextEditingController mfgDateController;
|
||||
final TextEditingController expDateController;
|
||||
|
||||
// State variables passed from Parent
|
||||
final WarehouseData? selectedWarehouse;
|
||||
|
||||
// Callbacks to update Parent State
|
||||
final Function(WarehouseData?) onWarehouseChanged;
|
||||
final Function(String from) onPriceChanged; // To trigger calculation
|
||||
final Function(String date) onMfgDateSelected;
|
||||
final Function(String date) onExpDateSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final permissionService = PermissionService(ref);
|
||||
final warehouseData = ref.watch(fetchWarehouseListProvider);
|
||||
final modules = snapShot.data?.modules;
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
///-------------Batch No & Warehouse----------------------------------
|
||||
if (modules?.showBatchNo == '1' || modules?.showWarehouse == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showBatchNo == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: batchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.batchNo,
|
||||
hintText: _lang.enterBatchNo,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showBatchNo == '1' && modules?.showWarehouse == '1') const SizedBox(width: 14),
|
||||
if (modules?.showWarehouse == '1')
|
||||
Expanded(
|
||||
child: warehouseData.when(
|
||||
data: (dataList) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
DropdownButtonFormField<WarehouseData>(
|
||||
hint: Text(_lang.selectWarehouse),
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.warehouse,
|
||||
),
|
||||
value: selectedWarehouse,
|
||||
icon: selectedWarehouse != null
|
||||
? IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
onWarehouseChanged.call(null);
|
||||
},
|
||||
)
|
||||
: const Icon(Icons.keyboard_arrow_down_outlined),
|
||||
items: dataList.data
|
||||
?.map(
|
||||
(rack) => DropdownMenuItem<WarehouseData>(
|
||||
value: rack,
|
||||
child: Text(
|
||||
rack.name ?? '',
|
||||
style: const TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: onWarehouseChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (e, st) => const Text('Warehouse Load Error'),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
// child: warehouseData.when(
|
||||
// data: (dataList) {
|
||||
// return DropdownButtonFormField<WarehouseData>(
|
||||
// hint: const Text('Select Warehouse'),
|
||||
// isExpanded: true,
|
||||
// decoration: const InputDecoration(labelText: 'Warehouse', border: OutlineInputBorder()),
|
||||
// value: selectedWarehouse,
|
||||
// icon: const Icon(Icons.keyboard_arrow_down_outlined),
|
||||
// items: dataList.data
|
||||
// ?.map(
|
||||
// (rack) => DropdownMenuItem<WarehouseData>(
|
||||
// value: rack,
|
||||
// child: Text(rack.name ?? '', style: const TextStyle(fontWeight: FontWeight.normal)),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// onChanged: onWarehouseChanged,
|
||||
// );
|
||||
// },
|
||||
// error: (e, st) => const Text('Rack Load Error'),
|
||||
// loading: () => const Center(child: CircularProgressIndicator()),
|
||||
// ),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
if (modules?.showProductStock == '1') const SizedBox(height: 24),
|
||||
|
||||
///-------------Stock--------------------------------------
|
||||
if (modules?.showProductStock == '1')
|
||||
TextFormField(
|
||||
controller: stockController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).stock,
|
||||
hintText: lang.S.of(context).enterStock,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
|
||||
///_________Purchase Price (Exclusive & Inclusive)____________________
|
||||
if ((modules?.showExclusivePrice == '1' || modules?.showInclusivePrice == '1') &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value)) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showExclusivePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: purchaseExController,
|
||||
onChanged: (value) => onPriceChanged('purchase_ex'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).purchaseEx,
|
||||
hintText: lang.S.of(context).enterPurchasePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showExclusivePrice == '1' && modules?.showInclusivePrice == '1') const SizedBox(width: 14),
|
||||
if (modules?.showInclusivePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: purchaseIncController,
|
||||
onChanged: (value) => onPriceChanged('purchase_inc'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).purchaseIn,
|
||||
hintText: lang.S.of(context).enterSaltingPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_________Profit Margin & MRP_____________________
|
||||
if (modules?.showProfitPercent == '1' || modules?.showProductSalePrice == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
(permissionService.hasPermission(Permit.productsPriceView.value)))
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: profitController,
|
||||
onChanged: (value) => onPriceChanged('profit_margin'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).profitMargin,
|
||||
hintText: lang.S.of(context).enterPurchasePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
modules?.showProductSalePrice == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
const SizedBox(width: 14),
|
||||
if (modules?.showProductSalePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: saleController,
|
||||
onChanged: (value) => onPriceChanged('mrp'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).mrp,
|
||||
hintText: lang.S.of(context).enterSaltingPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_______Wholesale & Dealer Price_________________
|
||||
if (modules?.showProductWholesalePrice == '1' || modules?.showProductDealerPrice == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showProductWholesalePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: wholesaleController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).wholeSalePrice,
|
||||
hintText: lang.S.of(context).enterWholesalePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showProductWholesalePrice == '1' && modules?.showProductDealerPrice == '1')
|
||||
const SizedBox(width: 14),
|
||||
if (modules?.showProductDealerPrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: dealerController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).dealerPrice,
|
||||
hintText: lang.S.of(context).enterDealerPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_______Dates_________________
|
||||
if ((modules?.showMfgDate == '1') || (modules?.showExpireDate == '1')) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showMfgDate == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: mfgDateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).manuDate,
|
||||
hintText: lang.S.of(context).selectDate,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
onMfgDateSelected(picked.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showMfgDate == '1' && modules?.showExpireDate == '1') const SizedBox(width: 14),
|
||||
if (modules?.showExpireDate == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: expDateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).expDate,
|
||||
hintText: lang.S.of(context).selectDate,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
onExpDateSelected(picked.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
898
lib/Screens/Products/add product/variant_product_form.dart
Normal file
898
lib/Screens/Products/add product/variant_product_form.dart
Normal file
@@ -0,0 +1,898 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/model/get_product_setting_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../../constant.dart';
|
||||
import '../../../currency.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../product variation/model/product_variation_model.dart';
|
||||
import '../../product variation/provider/product_variation_provider.dart';
|
||||
import '../../vat_&_tax/model/vat_model.dart';
|
||||
import '../../warehouse/warehouse_provider/warehouse_provider.dart';
|
||||
import '../Widgets/acnoo_multiple_select_dropdown.dart';
|
||||
import '../Widgets/dropdown_styles.dart';
|
||||
import 'modle/create_product_model.dart';
|
||||
|
||||
class VariantProductForm extends ConsumerStatefulWidget {
|
||||
const VariantProductForm({
|
||||
super.key,
|
||||
required this.initialStocks,
|
||||
required this.onStocksUpdated,
|
||||
required this.snapShot,
|
||||
this.selectedWarehouse,
|
||||
required this.onSelectVariation,
|
||||
this.tax,
|
||||
required this.taxType,
|
||||
this.productVariationIds,
|
||||
this.productCode,
|
||||
});
|
||||
|
||||
final List<StockDataModel> initialStocks;
|
||||
final Function(List<StockDataModel>) onStocksUpdated;
|
||||
final Function(List<String?>) onSelectVariation;
|
||||
final GetProductSettingModel snapShot;
|
||||
final VatModel? tax;
|
||||
final String taxType;
|
||||
final List<String>? productVariationIds;
|
||||
final String? productCode;
|
||||
// State variables passed from Parent
|
||||
final WarehouseData? selectedWarehouse; // Received from parent
|
||||
|
||||
@override
|
||||
ConsumerState<VariantProductForm> createState() => _VariantProductFormState();
|
||||
}
|
||||
|
||||
class _VariantProductFormState extends ConsumerState<VariantProductForm> {
|
||||
List<int?> selectedVariation = [];
|
||||
List<VariationData> variationList = [];
|
||||
Map<num?, List<String>?> selectedVariationValues = {};
|
||||
List<StockDataModel> localVariantStocks = [];
|
||||
|
||||
bool isDataInitialized = false;
|
||||
|
||||
final kLoader = const Center(child: CircularProgressIndicator(strokeWidth: 2));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
localVariantStocks = widget.initialStocks;
|
||||
}
|
||||
|
||||
void generateVariants({bool? changeState}) {
|
||||
if (selectedVariation.isEmpty) {
|
||||
setState(() => localVariantStocks.clear());
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
return;
|
||||
}
|
||||
// 1. Gather active Variations (No Change)
|
||||
List<VariationData> activeVariations = [];
|
||||
List<List<String>> activeValues = [];
|
||||
|
||||
for (var id in selectedVariation) {
|
||||
if (id != null &&
|
||||
selectedVariationValues.containsKey(id) &&
|
||||
selectedVariationValues[id] != null &&
|
||||
selectedVariationValues[id]!.isNotEmpty) {
|
||||
var vData = variationList.firstWhere((element) => element.id == id, orElse: () => VariationData());
|
||||
if (vData.id != null) {
|
||||
activeVariations.add(vData);
|
||||
activeValues.add(selectedVariationValues[id]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeVariations.isEmpty || activeValues.length != activeVariations.length) {
|
||||
setState(() => localVariantStocks = []);
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
return;
|
||||
}
|
||||
;
|
||||
|
||||
// 2. Calculate Cartesian Product (No Change)
|
||||
List<List<String>> cartesian(List<List<String>> lists) {
|
||||
List<List<String>> result = [[]];
|
||||
for (var list in lists) {
|
||||
result = [
|
||||
for (var a in result)
|
||||
for (var b in list) [...a, b]
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<List<String>> combinations = cartesian(activeValues);
|
||||
List<StockDataModel> newStocks = [];
|
||||
|
||||
String baseCode = widget.productCode ?? "";
|
||||
int counter = 1;
|
||||
for (var combo in combinations) {
|
||||
String variantName = combo.join(" - ");
|
||||
List<Map<String, String>> vData = [];
|
||||
for (int i = 0; i < combo.length; i++) {
|
||||
vData.add({activeVariations[i].name ?? '': combo[i]});
|
||||
}
|
||||
|
||||
// Check if this ROOT variant already exists (to preserve edits)
|
||||
var existingIndex = localVariantStocks.indexWhere((element) => element.variantName == variantName);
|
||||
|
||||
if (existingIndex != -1) {
|
||||
StockDataModel parent = localVariantStocks[existingIndex];
|
||||
|
||||
// Updating batch no according to new code structure
|
||||
if (baseCode.isNotEmpty) {
|
||||
parent.batchNo = "$baseCode-$counter";
|
||||
}
|
||||
newStocks.add(parent);
|
||||
} else {
|
||||
// C. New Root Variant
|
||||
String autoBatchNo = baseCode.isNotEmpty ? "$baseCode-$counter" : "";
|
||||
|
||||
newStocks.add(StockDataModel(
|
||||
profitPercent: '0',
|
||||
variantName: variantName,
|
||||
batchNo: autoBatchNo, // NEW LOGIC: 1002-1
|
||||
variationData: vData,
|
||||
productStock: "0",
|
||||
exclusivePrice: "0",
|
||||
inclusivePrice: "0",
|
||||
productSalePrice: "0",
|
||||
));
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
setState(() => localVariantStocks = newStocks);
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
// --- Logic to Initialize Data from Edit Mode ---
|
||||
void _initializeEditData(List<VariationData> allVariations) {
|
||||
if (isDataInitialized) return;
|
||||
if (localVariantStocks.isEmpty && (widget.productVariationIds == null || widget.productVariationIds!.isEmpty))
|
||||
return;
|
||||
|
||||
// 1. Set Selected Variation Types (Example: Size, Color IDs)
|
||||
if (widget.productVariationIds != null) {
|
||||
selectedVariation = widget.productVariationIds!.map((e) => int.tryParse(e)).where((e) => e != null).toList();
|
||||
}
|
||||
|
||||
for (final stock in localVariantStocks) {
|
||||
print('Pioewruwr------------------------> ${stock.variationData}');
|
||||
if (stock.variationData != null) {
|
||||
for (Map<String, dynamic> vMap in stock.variationData!) {
|
||||
print('$vMap');
|
||||
// vMap looks like {"Size": "M"}
|
||||
vMap.forEach((keyName, value) {
|
||||
// Find the ID associated with this Name (e.g., "Size" -> ID 1)
|
||||
final variationObj = allVariations.firstWhere(
|
||||
(element) => element.name?.toLowerCase() == keyName.toLowerCase(),
|
||||
orElse: () => VariationData(),
|
||||
);
|
||||
|
||||
if (variationObj.id != null) {
|
||||
num vId = variationObj.id!;
|
||||
|
||||
// Add value to the list if not exists
|
||||
if (!selectedVariationValues.containsKey(vId)) {
|
||||
selectedVariationValues[vId] = [];
|
||||
}
|
||||
|
||||
if (value is String && !selectedVariationValues[vId]!.contains(value)) {
|
||||
selectedVariationValues[vId]!.add(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isDataInitialized = true;
|
||||
Future.microtask(() => setState(() {}));
|
||||
}
|
||||
|
||||
void _addSubVariation(int parentIndex) {
|
||||
final parentStock = localVariantStocks[parentIndex];
|
||||
|
||||
// Ensure parent has a batch number
|
||||
if (parentStock.batchNo == null || parentStock.batchNo!.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Parent must have a Batch No first")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Count existing children to generate ID (e.g., 1001-1, 1001-2)
|
||||
final String parentBatch = parentStock.batchNo!;
|
||||
int childCount = localVariantStocks
|
||||
.where((element) => element.batchNo != null && element.batchNo!.startsWith("$parentBatch-"))
|
||||
.length;
|
||||
|
||||
String newSubBatch = "$parentBatch-${childCount + 1}";
|
||||
|
||||
// Create Child Stock (Copying basic data from parent if needed, or blank)
|
||||
StockDataModel childStock = StockDataModel(
|
||||
variantName: "${parentStock.variantName} (Sub ${childCount + 1})", // Indicating it's a sub
|
||||
batchNo: '',
|
||||
variationData: parentStock.variationData, // Inherit variation traits
|
||||
profitPercent: parentStock.profitPercent ?? '0',
|
||||
productStock: "0",
|
||||
exclusivePrice: parentStock.exclusivePrice ?? "0",
|
||||
inclusivePrice: parentStock.inclusivePrice ?? "0",
|
||||
productSalePrice: parentStock.productSalePrice ?? "0",
|
||||
warehouseId: parentStock.warehouseId,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
// Insert immediately after the parent (and its existing children)
|
||||
// We insert at parentIndex + 1 + childCount to keep them grouped
|
||||
localVariantStocks.insert(parentIndex + 1 + childCount, childStock);
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
void _removeVariation(int index) {
|
||||
final stockToRemove = localVariantStocks[index];
|
||||
final String? batchNo = stockToRemove.batchNo;
|
||||
|
||||
setState(() {
|
||||
localVariantStocks.removeAt(index);
|
||||
|
||||
// If it was a parent, remove all its children (Sub-variations)
|
||||
if (batchNo != null && !batchNo.contains('-')) {
|
||||
localVariantStocks
|
||||
.removeWhere((element) => element.batchNo != null && element.batchNo!.startsWith("$batchNo-"));
|
||||
}
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _dropdownStyle = AcnooDropdownStyle(context);
|
||||
final variationData = ref.watch(variationListProvider);
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
//------- Variation Type Selection --------------------
|
||||
variationData.when(
|
||||
data: (variation) {
|
||||
variationList = variation.data ?? [];
|
||||
|
||||
// -----------------------------------------
|
||||
// HERE IS THE FIX: Initialize Data Once
|
||||
// -----------------------------------------
|
||||
if (!isDataInitialized && variationList.isNotEmpty) {
|
||||
_initializeEditData(variationList);
|
||||
}
|
||||
|
||||
return AcnooMultiSelectDropdown(
|
||||
menuItemStyleData: _dropdownStyle.multiSelectMenuItemStyle,
|
||||
buttonStyleData: _dropdownStyle.buttonStyle,
|
||||
iconStyleData: _dropdownStyle.iconStyle,
|
||||
dropdownStyleData: _dropdownStyle.dropdownStyle,
|
||||
labelText: lang.S.of(context).selectVariations,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.all(8),
|
||||
hintText: lang.S.of(context).selectItems,
|
||||
),
|
||||
values: selectedVariation,
|
||||
items: variationList.map((item) {
|
||||
return MultiSelectDropdownMenuItem(value: item.id, labelText: item.name ?? '');
|
||||
}).toList(),
|
||||
onChanged: (values) {
|
||||
setState(() {
|
||||
selectedVariation = values?.map((e) => e as int?).toList() ?? [];
|
||||
|
||||
selectedVariationValues.removeWhere((key, value) => !selectedVariation.contains(key));
|
||||
});
|
||||
|
||||
widget.onSelectVariation(values?.map((e) => e.toString()).toList() ?? []);
|
||||
if (selectedVariation.isEmpty) {
|
||||
setState(() => localVariantStocks.clear());
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
} else {
|
||||
generateVariants();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
loading: () => kLoader,
|
||||
),
|
||||
|
||||
//----------- Variation Values Selection ---------------
|
||||
if (selectedVariation.isNotEmpty) const SizedBox(height: 24),
|
||||
if (selectedVariation.isNotEmpty)
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: variationList.where((item) => selectedVariation.contains(item.id)).length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, mainAxisSpacing: 8, crossAxisSpacing: 8, childAspectRatio: 2.8),
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems = variationList.where((item) => selectedVariation.contains(item.id)).toList();
|
||||
final varItem = filteredItems[index];
|
||||
return AcnooMultiSelectDropdown<String>(
|
||||
key: GlobalKey(debugLabel: varItem.name),
|
||||
labelText: varItem.name ?? '',
|
||||
values: selectedVariationValues[varItem.id] ?? [],
|
||||
items: (varItem.values ?? []).map((value) {
|
||||
return MultiSelectDropdownMenuItem(value: value, labelText: value);
|
||||
}).toList(),
|
||||
onChanged: (values) {
|
||||
selectedVariationValues[varItem.id?.toInt()] = values != null && values.isNotEmpty ? values : null;
|
||||
|
||||
generateVariants(changeState: false);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (selectedVariation.isEmpty) const SizedBox(height: 24),
|
||||
|
||||
// ================= GENERATED VARIANT LIST =================
|
||||
if (localVariantStocks.isNotEmpty) ...[
|
||||
// const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"${lang.S.of(context).selectVariations} (${localVariantStocks.length})",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: localVariantStocks.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final stock = localVariantStocks[index];
|
||||
// Check if this is a Sub-Variation (contains '-')
|
||||
bool isSubVariation = stock.batchNo != null && stock.variantName!.contains('Sub');
|
||||
|
||||
return Container(
|
||||
color: isSubVariation ? Colors.grey.shade50 : Colors.transparent, // Light bg for sub items
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
showVariantEditSheet(
|
||||
context: context,
|
||||
stock: localVariantStocks[index],
|
||||
snapShot: widget.snapShot,
|
||||
tax: widget.tax,
|
||||
taxType: widget.taxType,
|
||||
onSave: (updatedStock) {
|
||||
setState(() {
|
||||
localVariantStocks[index] = updatedStock;
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
},
|
||||
);
|
||||
},
|
||||
contentPadding: !isSubVariation ? EdgeInsets.zero : EdgeInsetsDirectional.only(start: 30),
|
||||
// (+) Button only for Parent items
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: !isSubVariation
|
||||
? IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
icon: const Icon(Icons.add, color: kTitleColor),
|
||||
tooltip: lang.S.of(context).addSubVariation,
|
||||
onPressed: () => _addSubVariation(index),
|
||||
)
|
||||
: Icon(Icons.subdirectory_arrow_right,
|
||||
color: Colors.grey, size: 18), // Visual indicator for child
|
||||
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
stock.variantName ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text.rich(TextSpan(
|
||||
text: '${lang.S.of(context).stock}: ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: stock.productStock ?? 'n/a',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
color: kPeraColor),
|
||||
)
|
||||
])),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${lang.S.of(context).batchNo}: ${stock.batchNo ?? 'N/A'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium
|
||||
?.copyWith(fontSize: isSubVariation ? 13 : 14, color: kPeraColor),
|
||||
),
|
||||
),
|
||||
Text.rich(TextSpan(
|
||||
text: '${lang.S.of(context).sale}: ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$currency${stock.productSalePrice ?? 'n/a'}',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
color: kTitleColor,
|
||||
),
|
||||
)
|
||||
])),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showVariantEditSheet(
|
||||
context: context,
|
||||
stock: localVariantStocks[index],
|
||||
snapShot: widget.snapShot,
|
||||
tax: widget.tax,
|
||||
taxType: widget.taxType,
|
||||
onSave: (updatedStock) {
|
||||
setState(() {
|
||||
localVariantStocks[index] = updatedStock;
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
},
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
_removeVariation(index);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Show delete only if sub-variation
|
||||
if (isSubVariation)
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showVariantEditSheet({
|
||||
required BuildContext context,
|
||||
required StockDataModel stock,
|
||||
required GetProductSettingModel snapShot,
|
||||
VatModel? tax,
|
||||
required String taxType,
|
||||
required Function(StockDataModel updatedStock) onSave,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (context) =>
|
||||
VariantEditSheet(stock: stock, snapShot: snapShot, tax: tax, taxType: taxType, onSave: onSave),
|
||||
);
|
||||
}
|
||||
|
||||
class VariantEditSheet extends ConsumerStatefulWidget {
|
||||
const VariantEditSheet(
|
||||
{super.key,
|
||||
required this.stock,
|
||||
required this.snapShot,
|
||||
required this.tax,
|
||||
required this.taxType,
|
||||
required this.onSave});
|
||||
final StockDataModel stock;
|
||||
final GetProductSettingModel snapShot;
|
||||
final VatModel? tax;
|
||||
final String taxType;
|
||||
final Function(StockDataModel) onSave;
|
||||
@override
|
||||
ConsumerState<VariantEditSheet> createState() => _VariantEditSheetState();
|
||||
}
|
||||
|
||||
class _VariantEditSheetState extends ConsumerState<VariantEditSheet> {
|
||||
late TextEditingController productBatchNumberController;
|
||||
late TextEditingController productStockController;
|
||||
late TextEditingController purchaseExclusivePriceController;
|
||||
late TextEditingController purchaseInclusivePriceController;
|
||||
late TextEditingController profitMarginController;
|
||||
late TextEditingController salePriceController;
|
||||
late TextEditingController wholeSalePriceController;
|
||||
late TextEditingController dealerPriceController;
|
||||
late TextEditingController expireDateController;
|
||||
late TextEditingController manufactureDateController;
|
||||
|
||||
String? selectedExpireDate;
|
||||
String? selectedManufactureDate;
|
||||
String? selectedWarehouseId; // Added variable for Warehouse
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
productBatchNumberController = TextEditingController(text: widget.stock.batchNo ?? '');
|
||||
productStockController = TextEditingController(text: widget.stock.productStock ?? '');
|
||||
purchaseExclusivePriceController = TextEditingController(text: widget.stock.exclusivePrice ?? '');
|
||||
purchaseInclusivePriceController = TextEditingController(text: widget.stock.inclusivePrice ?? '');
|
||||
profitMarginController = TextEditingController(text: widget.stock.profitPercent ?? '');
|
||||
salePriceController = TextEditingController(text: widget.stock.productSalePrice ?? '');
|
||||
wholeSalePriceController = TextEditingController(text: widget.stock.productWholeSalePrice ?? '');
|
||||
dealerPriceController = TextEditingController(text: widget.stock.productDealerPrice ?? '');
|
||||
selectedExpireDate = widget.stock.expireDate;
|
||||
selectedManufactureDate = widget.stock.mfgDate;
|
||||
|
||||
// Initialize Warehouse ID
|
||||
selectedWarehouseId = widget.stock.warehouseId;
|
||||
|
||||
expireDateController = TextEditingController(
|
||||
text: selectedExpireDate != null && selectedExpireDate!.isNotEmpty
|
||||
? DateFormat.yMd().format(DateTime.parse(selectedExpireDate!))
|
||||
: '');
|
||||
manufactureDateController = TextEditingController(
|
||||
text: selectedManufactureDate != null && selectedManufactureDate!.isNotEmpty
|
||||
? DateFormat.yMd().format(DateTime.parse(selectedManufactureDate!))
|
||||
: '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
productBatchNumberController.dispose();
|
||||
productStockController.dispose();
|
||||
purchaseExclusivePriceController.dispose();
|
||||
purchaseInclusivePriceController.dispose();
|
||||
profitMarginController.dispose();
|
||||
salePriceController.dispose();
|
||||
wholeSalePriceController.dispose();
|
||||
dealerPriceController.dispose();
|
||||
expireDateController.dispose();
|
||||
manufactureDateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void calculatePurchaseAndMrp({String? from}) {
|
||||
num taxRate = widget.tax?.rate ?? 0;
|
||||
num purchaseExc = num.tryParse(purchaseExclusivePriceController.text) ?? 0;
|
||||
num purchaseInc = num.tryParse(purchaseInclusivePriceController.text) ?? 0;
|
||||
num profitMargin = num.tryParse(profitMarginController.text) ?? 0;
|
||||
num salePrice = num.tryParse(salePriceController.text) ?? 0;
|
||||
|
||||
if (from == 'purchase_inc') {
|
||||
purchaseExc = (taxRate != 0) ? purchaseInc / (1 + taxRate / 100) : purchaseInc;
|
||||
purchaseExclusivePriceController.text = purchaseExc.toStringAsFixed(2);
|
||||
} else {
|
||||
purchaseInc = purchaseExc + (purchaseExc * taxRate / 100);
|
||||
purchaseInclusivePriceController.text = purchaseInc.toStringAsFixed(2);
|
||||
}
|
||||
purchaseExc = num.tryParse(purchaseExclusivePriceController.text) ?? 0;
|
||||
purchaseInc = num.tryParse(purchaseInclusivePriceController.text) ?? 0;
|
||||
num basePrice = widget.taxType.toLowerCase() == 'exclusive' ? purchaseExc : purchaseInc;
|
||||
|
||||
if (from == 'mrp') {
|
||||
salePrice = num.tryParse(salePriceController.text) ?? 0;
|
||||
if (basePrice > 0) {
|
||||
profitMargin = ((salePrice - basePrice) / basePrice) * 100;
|
||||
profitMarginController.text = profitMargin.toStringAsFixed(2);
|
||||
}
|
||||
} else {
|
||||
if (basePrice > 0) {
|
||||
salePrice = basePrice + (basePrice * profitMargin / 100);
|
||||
salePriceController.text = salePrice.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final permissionService = PermissionService(ref);
|
||||
final theme = Theme.of(context);
|
||||
final modules = widget.snapShot.data?.modules;
|
||||
|
||||
// 1. Fetch Warehouse List from Provider
|
||||
final warehouseData = ref.watch(fetchWarehouseListProvider);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration:
|
||||
const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Flexible(
|
||||
child: Text('${lang.S.of(context).edit} ${widget.stock.variantName}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, fontSize: 18)),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close, size: 20, color: Colors.grey))
|
||||
])),
|
||||
const Divider(height: 1, color: kBorderColor),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(children: [
|
||||
// 2. Display Warehouse Dropdown
|
||||
warehouseData.when(
|
||||
data: (data) => DropdownButtonFormField<String>(
|
||||
value: selectedWarehouseId,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).warehouse,
|
||||
hintText: lang.S.of(context).selectWarehouse,
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12)),
|
||||
items: data.data
|
||||
?.map((WarehouseData w) =>
|
||||
DropdownMenuItem<String>(value: w.id.toString(), child: Text(w.name ?? '')))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => selectedWarehouseId = v)),
|
||||
error: (e, s) => const Text('Failed to load warehouse'),
|
||||
loading: () => const Center(child: LinearProgressIndicator())),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (modules?.showBatchNo == '1' || modules?.showProductStock == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showBatchNo == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: productBatchNumberController,
|
||||
label: lang.S.of(context).batchNo,
|
||||
hint: "Ex: B-001")),
|
||||
if (modules?.showBatchNo == '1' && modules?.showProductStock == '1') const SizedBox(width: 12),
|
||||
if (modules?.showProductStock == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: productStockController,
|
||||
label: lang.S.of(context).stock,
|
||||
isNumber: true,
|
||||
hint: "Ex: 50"))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if ((modules?.showExclusivePrice == '1' || modules?.showInclusivePrice == '1') &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value)) ...[
|
||||
Row(children: [
|
||||
if (modules?.showExclusivePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: purchaseExclusivePriceController,
|
||||
label: lang.S.of(context).purchaseEx,
|
||||
isNumber: true,
|
||||
hint: "Ex: 100.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp())),
|
||||
if (modules?.showExclusivePrice == '1' && modules?.showInclusivePrice == '1')
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showInclusivePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: purchaseInclusivePriceController,
|
||||
label: lang.S.of(context).purchaseIn,
|
||||
isNumber: true,
|
||||
hint: "Ex: 115.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp(from: "purchase_inc")))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showProfitPercent == '1' || modules?.showProductSalePrice == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: profitMarginController,
|
||||
label: lang.S.of(context).profitMargin,
|
||||
isNumber: true,
|
||||
hint: "Ex: 20%",
|
||||
onChanged: (v) => calculatePurchaseAndMrp())),
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
modules?.showProductSalePrice == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showProductSalePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: salePriceController,
|
||||
label: lang.S.of(context).mrp,
|
||||
isNumber: true,
|
||||
hint: "Ex: 150.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp(from: 'mrp')))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showProductWholesalePrice == '1' || modules?.showProductDealerPrice == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showProductWholesalePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: wholeSalePriceController,
|
||||
label: lang.S.of(context).wholeSalePrice,
|
||||
isNumber: true,
|
||||
hint: "Ex: 130.00")),
|
||||
if (modules?.showProductWholesalePrice == '1' && modules?.showProductDealerPrice == '1')
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showProductDealerPrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: dealerPriceController,
|
||||
label: lang.S.of(context).dealerPrice,
|
||||
isNumber: true,
|
||||
hint: "Ex: 120.00"))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showMfgDate == '1' || modules?.showExpireDate == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showMfgDate == '1')
|
||||
Expanded(
|
||||
child: _buildDateField(
|
||||
controller: manufactureDateController,
|
||||
label: lang.S.of(context).manufactureDate,
|
||||
isExpire: false,
|
||||
hint: lang.S.of(context).selectDate)),
|
||||
if (modules?.showMfgDate == '1' && modules?.showExpireDate == '1') const SizedBox(width: 12),
|
||||
if (modules?.showExpireDate == '1')
|
||||
Expanded(
|
||||
child: _buildDateField(
|
||||
controller: expireDateController,
|
||||
label: lang.S.of(context).expDate,
|
||||
isExpire: true,
|
||||
hint: lang.S.of(context).selectDate,
|
||||
))
|
||||
]),
|
||||
const SizedBox(height: 24)
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// 3. Set the selected warehouse ID to the stock object
|
||||
widget.stock.warehouseId = selectedWarehouseId;
|
||||
|
||||
widget.stock.batchNo = productBatchNumberController.text;
|
||||
widget.stock.productStock = productStockController.text;
|
||||
widget.stock.exclusivePrice = purchaseExclusivePriceController.text;
|
||||
widget.stock.inclusivePrice = purchaseInclusivePriceController.text;
|
||||
widget.stock.profitPercent = profitMarginController.text;
|
||||
widget.stock.productSalePrice = salePriceController.text;
|
||||
widget.stock.productWholeSalePrice = wholeSalePriceController.text;
|
||||
widget.stock.productDealerPrice = dealerPriceController.text;
|
||||
widget.stock.expireDate = selectedExpireDate;
|
||||
widget.stock.mfgDate = selectedManufactureDate;
|
||||
widget.onSave(widget.stock);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(lang.S.of(context).saveVariant))),
|
||||
const SizedBox(height: 16),
|
||||
]),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField(
|
||||
{required TextEditingController controller,
|
||||
required String label,
|
||||
String? hint,
|
||||
bool isNumber = false,
|
||||
Function(String)? onChanged}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
|
||||
inputFormatters: isNumber ? [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))] : [],
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)));
|
||||
}
|
||||
|
||||
Widget _buildDateField(
|
||||
{required TextEditingController controller, required String label, String? hint, required bool isExpire}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
suffixIcon: const Icon(Icons.calendar_today, size: 18)),
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context, initialDate: DateTime.now(), firstDate: DateTime(2015, 8), lastDate: DateTime(2101));
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
controller.text = DateFormat.yMd().format(picked);
|
||||
if (isExpire) {
|
||||
selectedExpireDate = picked.toString();
|
||||
} else {
|
||||
selectedManufactureDate = picked.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'dart:io';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/Screens/Products/bulk%20product%20upload/repo/bulk_upload_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
import '../../../GlobalComponents/glonal_popup.dart';
|
||||
|
||||
class BulkUploader extends StatefulWidget {
|
||||
const BulkUploader({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BulkUploader> createState() => _BulkUploaderState();
|
||||
}
|
||||
|
||||
class _BulkUploaderState extends State<BulkUploader> {
|
||||
File? file;
|
||||
|
||||
String getFileExtension(String fileName) {
|
||||
return fileName.split('/').last;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = lang.S.of(context);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_lang.excelUploader),
|
||||
),
|
||||
body: Consumer(builder: (context, ref, __) {
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
return businessInfo.when(data: (details) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: file != null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: const Image(image: AssetImage('images/excel.png'))),
|
||||
title: Text(
|
||||
getFileExtension(file?.path ?? ''),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
file = null;
|
||||
});
|
||||
},
|
||||
child: Text(_lang.remove)))),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: file == null,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: Image(
|
||||
height: 100,
|
||||
width: 100,
|
||||
image: AssetImage('images/file-upload.png'),
|
||||
)),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: const ButtonStyle(backgroundColor: WidgetStatePropertyAll(kMainColor)),
|
||||
onPressed: () async {
|
||||
if (file == null) {
|
||||
await pickAndUploadFile(ref: ref);
|
||||
} else {
|
||||
EasyLoading.show(status: _lang.uploading);
|
||||
await BulkUpLoadRepo().uploadBulkFile(file: file!, ref: ref, context: context);
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
},
|
||||
child: Text(file == null ? _lang.pickAndUploadFile : _lang.upload,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await BulkUpLoadRepo().downloadFile(context);
|
||||
},
|
||||
child: Text(_lang.downloadExcelFormat),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
Future<void> pickAndUploadFile({required WidgetRef ref}) async {
|
||||
XTypeGroup typeGroup = XTypeGroup(
|
||||
label: lang.S.of(context).excelFiles,
|
||||
extensions: ['xlsx'],
|
||||
);
|
||||
final XFile? fileResult = await openFile(acceptedTypeGroups: [typeGroup]);
|
||||
|
||||
if (fileResult != null) {
|
||||
final File files = File(fileResult.path);
|
||||
setState(() {
|
||||
file = files;
|
||||
});
|
||||
} else {
|
||||
print(lang.S.of(context).noFileSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/Screens/product_brand/product_brand_provider/product_brand_provider.dart';
|
||||
import 'package:mobile_pos/Screens/product_category/provider/product_category_provider/product_unit_provider.dart';
|
||||
import 'package:mobile_pos/Screens/product_unit/provider/product_unit_provider.dart';
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../Repository/constant_functions.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class BulkUpLoadRepo {
|
||||
Future<void> uploadBulkFile({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required File file,
|
||||
}) async {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
final uri = Uri.parse('${APIConfig.url}/bulk-uploads');
|
||||
|
||||
var request = http.MultipartRequest('POST', uri)
|
||||
..headers['Accept'] = 'application/json'
|
||||
..headers['Authorization'] = await getAuthToken();
|
||||
|
||||
request.files.add(http.MultipartFile.fromBytes('file', file.readAsBytesSync(), filename: file.path));
|
||||
|
||||
final response = await customHttpClient.uploadFile(url: uri, fileFieldName: 'file', file: file, fields: request.fields);
|
||||
final responseData = await response.stream.bytesToString();
|
||||
final parsedData = jsonDecode(responseData);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successful!')));
|
||||
ref.refresh(productProvider);
|
||||
ref.refresh(categoryProvider);
|
||||
ref.refresh(brandsProvider);
|
||||
ref.refresh(unitsProvider);
|
||||
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed: ${parsedData['message']}')));
|
||||
}
|
||||
}
|
||||
|
||||
final String fileUrl = '${APIConfig.domain}assets/POSpro_bulk_product_upload.xlsx';
|
||||
|
||||
Future<void> downloadFile(BuildContext context) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse(fileUrl));
|
||||
if (response.statusCode != 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to download file!')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final downloadPath = '/storage/emulated/0/Download';
|
||||
final file = File('$downloadPath/POSpro_bulk_product_upload.xlsx');
|
||||
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('File saved to: ${file.path}')),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Download error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
985
lib/Screens/Products/product_details.dart
Normal file
985
lib/Screens/Products/product_details.dart
Normal file
@@ -0,0 +1,985 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/Screens/Products/add%20product/modle/create_product_model.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/product_provider.dart';
|
||||
import '../../service/check_actions_when_no_branch.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../widgets/key_values/key_values_widget.dart';
|
||||
import '../Purchase/Repo/purchase_repo.dart';
|
||||
import '../Purchase/purchase_product_buttom_sheet.dart';
|
||||
import '../hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import 'Repo/product_repo.dart';
|
||||
import 'add product/add_edit_comboItem.dart';
|
||||
import 'add product/add_product.dart';
|
||||
import 'add product/combo_product_form.dart';
|
||||
|
||||
class ProductDetails extends ConsumerStatefulWidget {
|
||||
const ProductDetails({
|
||||
super.key,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
final Product details;
|
||||
|
||||
@override
|
||||
ConsumerState<ProductDetails> createState() => _ProductDetailsState();
|
||||
}
|
||||
|
||||
class _ProductDetailsState extends ConsumerState<ProductDetails> {
|
||||
TextEditingController productStockController = TextEditingController();
|
||||
TextEditingController salePriceController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final providerData = ref.watch(fetchProductDetails(widget.details.id.toString()));
|
||||
final permissionService = PermissionService(ref);
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return GlobalPopup(
|
||||
child: providerData.when(data: (snapshot) {
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
backgroundColor: kWhite,
|
||||
surfaceTintColor: kWhite,
|
||||
title: Text(
|
||||
lang.S.of(context).productDetails,
|
||||
//'Product Details',
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddProduct(
|
||||
productModel: snapshot,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Colors.green,
|
||||
size: 22,
|
||||
)),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
bool confirmDelete = await showDeleteConfirmationDialog(context: context, itemName: 'product');
|
||||
if (confirmDelete) {
|
||||
EasyLoading.show(
|
||||
status: lang.S.of(context).deleting,
|
||||
);
|
||||
ProductRepo productRepo = ProductRepo();
|
||||
await productRepo.deleteProduct(id: snapshot.id.toString(), context: context, ref: ref);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete02,
|
||||
color: kMainColor,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
],
|
||||
centerTitle: true,
|
||||
// iconTheme: const IconThemeData(color: Colors.white),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Container(
|
||||
alignment: Alignment.topCenter,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(topRight: Radius.circular(30), topLeft: Radius.circular(30))),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (permissionService.hasPermission(Permit.productsRead.value)) ...{
|
||||
Container(
|
||||
height: 256,
|
||||
padding: EdgeInsets.all(8),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Color(0xffF5F3F3),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffF5F3F3),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
image: snapshot.productPicture == null
|
||||
? DecorationImage(fit: BoxFit.cover, image: AssetImage(noProductImageUrl))
|
||||
: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage('${APIConfig.domain}${snapshot.productPicture}'))),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
snapshot.productName.toString(),
|
||||
//'Smart watch',
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
snapshot.category?.categoryName.toString() ?? 'n/a',
|
||||
//'Apple Watch',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: const Color(0xffFEF0F1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
//Single product details-------------------------
|
||||
if (snapshot.productType == 'single')
|
||||
...{
|
||||
_lang.skuOrCode: snapshot.productCode ?? 'n/a',
|
||||
_lang.brand: snapshot.brand?.brandName ?? 'n/a',
|
||||
_lang.model: snapshot.productModel?.name ?? 'n/a',
|
||||
_lang.units: snapshot.unit?.unitName ?? 'n/a',
|
||||
_lang.rack: snapshot.rack?.name ?? 'n/a',
|
||||
_lang.shelf: snapshot.shelf?.name ?? 'n/a',
|
||||
// 'Test': snapshot.shelfId ?? 'n/a',
|
||||
_lang.stock: snapshot.stocksSumProductStock?.toString() ?? '0',
|
||||
_lang.lowStockAlert: snapshot.alertQty?.toString() ?? 'n/a',
|
||||
_lang.warehouse: snapshot.stocks?.first.warehouse?.name?.toString() ?? 'n/a',
|
||||
_lang.taxType: snapshot.vatType ?? 'n/a',
|
||||
_lang.tax: snapshot.vatAmount?.toString() ?? 'n/a',
|
||||
_lang.costExclusionTax: (snapshot.vatType != 'exclusive')
|
||||
? (snapshot.stocks != null &&
|
||||
snapshot.stocks!.isNotEmpty &&
|
||||
snapshot.stocks!.first.productPurchasePrice != null &&
|
||||
snapshot.vatAmount != null
|
||||
? '${snapshot.stocks!.first.productPurchasePrice! - snapshot.vatAmount!}'
|
||||
: '0')
|
||||
: ('$currency${snapshot.stocks?.isNotEmpty == true ? snapshot.stocks!.first.productPurchasePrice ?? '0' : '0'}'),
|
||||
_lang.costInclusionTax: (snapshot.vatType == 'exclusive')
|
||||
? (snapshot.stocks != null &&
|
||||
snapshot.stocks!.isNotEmpty &&
|
||||
snapshot.stocks!.first.productPurchasePrice != null &&
|
||||
snapshot.vatAmount != null
|
||||
? '$currency${snapshot.stocks!.first.productPurchasePrice! + snapshot.vatAmount!}'
|
||||
: '0')
|
||||
: ('$currency${snapshot.stocks?.isNotEmpty == true ? snapshot.stocks!.first.productPurchasePrice ?? '0' : '0'}'),
|
||||
'${_lang.profitMargin} (%)': (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.profitPercent != null
|
||||
? snapshot.stocks!.first.profitPercent.toString()
|
||||
: '0'),
|
||||
_lang.mrpOrSalePrice: (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.productSalePrice != null
|
||||
? '$currency${snapshot.stocks!.first.productSalePrice}'
|
||||
: '0'),
|
||||
_lang.wholeSalePrice: (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.productWholeSalePrice != null
|
||||
? '$currency${snapshot.stocks!.first.productWholeSalePrice}'
|
||||
: '0'),
|
||||
_lang.dealerPrice: (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.productDealerPrice != null
|
||||
? '$currency${snapshot.stocks?.first.productDealerPrice}'
|
||||
: '0'),
|
||||
_lang.manufactureDate:
|
||||
(snapshot.stocks?.isNotEmpty == true && snapshot.stocks!.first.mfgDate != null)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.parse(snapshot.stocks!.first.mfgDate!))
|
||||
: 'n/a',
|
||||
_lang.expiredDate:
|
||||
(snapshot.stocks?.isNotEmpty == true && snapshot.stocks!.first.expireDate != null)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.parse(snapshot.stocks?.first.expireDate ?? ''))
|
||||
: 'n/a',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.warrantyDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.warrantyUnit?.toString() ?? 'n/a'}',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.guaranteeDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.guaranteeUnit?.toString() ?? 'n/a'}',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
),
|
||||
//---------------variant product----------------
|
||||
if (snapshot.productType == 'variant')
|
||||
...{
|
||||
_lang.skuOrCode: snapshot.productCode ?? 'n/a',
|
||||
_lang.brand: snapshot.brand?.brandName ?? 'n/a',
|
||||
_lang.model: snapshot.productModel?.name ?? 'n/a',
|
||||
_lang.rack: snapshot.shelf?.name ?? 'n/a',
|
||||
_lang.lowStockAlert: snapshot.alertQty?.toString() ?? 'n/a',
|
||||
_lang.taxReport: snapshot.vatType ?? 'n/a',
|
||||
_lang.tax: snapshot.vatAmount?.toString() ?? 'n/a',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.warrantyDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.warrantyUnit?.toString() ?? 'n/a'}',
|
||||
_lang.guarantee:
|
||||
'${snapshot.warrantyGuaranteeInfo?.guaranteeDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.guaranteeUnit?.toString() ?? 'n/a'}',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
),
|
||||
//---------------Combo product----------------
|
||||
if (snapshot.productType == 'combo')
|
||||
...{
|
||||
_lang.skuOrCode: snapshot.productCode ?? 'n/a',
|
||||
_lang.brand: snapshot.brand?.brandName ?? 'n/a',
|
||||
_lang.model: snapshot.productModel?.name ?? 'n/a',
|
||||
_lang.units: snapshot.unit?.unitName ?? 'n/a',
|
||||
_lang.rack: snapshot.rack?.name ?? 'n/a',
|
||||
_lang.shelf: snapshot.shelf?.name ?? 'n/a',
|
||||
_lang.lowStockAlert: snapshot.alertQty?.toString() ?? 'n/a',
|
||||
_lang.type: snapshot.productType ?? 'n/a',
|
||||
_lang.taxType: snapshot.vatType ?? 'n/a',
|
||||
_lang.tax: snapshot.vatAmount?.toString() ?? 'n/a',
|
||||
_lang.netTotalAmount:
|
||||
(snapshot.productSalePrice != null && snapshot.profitPercent != null)
|
||||
? (snapshot.productSalePrice! / (1 + (snapshot.profitPercent! / 100)))
|
||||
.toStringAsFixed(2)
|
||||
: 'n/a',
|
||||
'${_lang.profitMargin} (%)': '${snapshot.profitPercent ?? 0}%',
|
||||
_lang.sellingPrice: '$currency${snapshot.productSalePrice ?? 0}',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.warrantyDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.warrantyUnit?.toString() ?? 'n/a'}',
|
||||
_lang.guarantee:
|
||||
'${snapshot.warrantyGuaranteeInfo?.guaranteeDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.guaranteeUnit?.toString() ?? 'n/a'}',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//--------------variant product details---------------------------------
|
||||
if (snapshot.productType == 'variant') ...[
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16, end: 16, top: 16, bottom: 6),
|
||||
child: Text(
|
||||
_lang.variationsProduct,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.separated(
|
||||
// padding: EdgeInsetsGeometry.symmetric(vertical: 10, horizontal: 16),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: snapshot.stocks?.length ?? 0,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
thickness: 0.3,
|
||||
color: kBorderColorTextField,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
snapshot.stocks?[index].variantName ?? 'n/a',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
// fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${_lang.sale}: $currency${snapshot.stocks?[index].productSalePrice ?? '0'}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${_lang.batch}: ${snapshot.stocks?[index].batchNo ?? 'N/A'}',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
// fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: '${_lang.stock}: ',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kNeutralColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: snapshot.stocks?[index].productStock.toString() ?? '0',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Color(0xff34C759),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
iconColor: kPeraColor,
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'view':
|
||||
viewModal(context, snapshot, index);
|
||||
break;
|
||||
case 'edit':
|
||||
final stock = snapshot.stocks?[index];
|
||||
|
||||
final cartProduct = CartProductModelPurchase(
|
||||
productId: snapshot.id ?? 0,
|
||||
variantName: stock?.variantName,
|
||||
brandName: snapshot.brand?.brandName,
|
||||
productName: snapshot.productName ?? '',
|
||||
productDealerPrice: stock?.productDealerPrice,
|
||||
productPurchasePrice: stock?.productPurchasePrice,
|
||||
productSalePrice: stock?.productSalePrice,
|
||||
productWholeSalePrice: stock?.productWholeSalePrice,
|
||||
quantities: stock?.productStock,
|
||||
productType: snapshot.productType ?? '',
|
||||
vatAmount: snapshot.vatAmount ?? 0,
|
||||
vatRate: snapshot.vat?.rate ?? 0,
|
||||
vatType: snapshot.vatType ?? 'exclusive',
|
||||
expireDate: stock?.expireDate,
|
||||
mfgDate: stock?.mfgDate,
|
||||
profitPercent: stock?.profitPercent ?? 0,
|
||||
stock: stock?.productStock,
|
||||
batchNumber: stock?.batchNo ?? '',
|
||||
);
|
||||
addProductInPurchaseCartButtomSheet(
|
||||
context: context,
|
||||
product: cartProduct,
|
||||
ref: ref,
|
||||
fromUpdate: false,
|
||||
index: index,
|
||||
fromStock: true,
|
||||
stocks: []);
|
||||
break;
|
||||
case 'add_stock':
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
productStockController.text = '1';
|
||||
salePriceController.text =
|
||||
snapshot.stocks?[index].productSalePrice?.toString() ?? '0.0';
|
||||
addStockPopUp(context, _formKey, theme, snapshot, index);
|
||||
break;
|
||||
case 'delete':
|
||||
showEditDeletePopUp(
|
||||
context: context,
|
||||
data: snapshot.stocks?[index],
|
||||
ref: ref,
|
||||
productId: widget.details.id.toString());
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'view', child: Text(_lang.view)),
|
||||
PopupMenuItem(value: 'edit', child: Text(_lang.edit)),
|
||||
PopupMenuItem(value: 'add_stock', child: Text(_lang.addStock)),
|
||||
PopupMenuItem(value: 'delete', child: Text(_lang.delete)),
|
||||
],
|
||||
),
|
||||
),
|
||||
visualDensity: VisualDensity(vertical: -4, horizontal: -4),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
//--------------Combo product details---------------------------------
|
||||
if (snapshot.productType == 'combo') ...[
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16, end: 16, top: 16, bottom: 6),
|
||||
child: Text(
|
||||
_lang.comboProducts,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.separated(
|
||||
// padding: EdgeInsetsGeometry.symmetric(vertical: 10, horizontal: 16),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: snapshot.comboProducts?.length ?? 0,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
thickness: 0.3,
|
||||
color: kBorderColorTextField,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final combo = snapshot.comboProducts![index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${combo.stock?.product?.productName ?? 'n/a'} ${combo.stock?.variantName ?? ''}',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${_lang.qty}: ${combo.quantity ?? '0'}',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${_lang.code}: ${combo.stock?.product?.productCode ?? 'n/a'}, ${_lang.batchNo}: ${snapshot.comboProducts?[index].stock?.batchNo ?? 'n/a'}',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${combo.stock?.productSalePrice ?? 0}',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// trailing: SizedBox(
|
||||
// width: 30,
|
||||
// child: PopupMenuButton<String>(
|
||||
// iconColor: kPeraColor,
|
||||
// onSelected: (value) async {
|
||||
// switch (value) {
|
||||
// case 'edit':
|
||||
// // Convert ComboProductComponent → ComboItem
|
||||
// final comboItem = ComboItem(
|
||||
// product: combo.product!,
|
||||
// stockData: combo.stock!,
|
||||
// quantity: combo.quantity ?? combo.stock?.productStock ?? 1,
|
||||
// manualPurchasePrice: combo.purchasePrice?.toDouble(),
|
||||
// );
|
||||
//
|
||||
// // Navigate to edit page
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => AddOrEditComboItem(
|
||||
// existingItem: comboItem,
|
||||
// onSubmit: (updatedItem) {
|
||||
// setState(() {
|
||||
// // Convert ComboItem → ComboProductComponent after edit
|
||||
// snapshot.comboProducts![index] = ComboProductComponent(
|
||||
// id: combo.id,
|
||||
// productId: updatedItem.product.id,
|
||||
// stockId: updatedItem.stockData.id,
|
||||
// purchasePrice: updatedItem.manualPurchasePrice,
|
||||
// quantity: updatedItem.quantity,
|
||||
// stock: updatedItem.stockData,
|
||||
// product: updatedItem.product,
|
||||
// );
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// break;
|
||||
//
|
||||
// case 'delete':
|
||||
// final confirmDelete = await showDialog<bool>(
|
||||
// context: context,
|
||||
// builder: (context) => AlertDialog(
|
||||
// title: const Text('Delete Combo Product'),
|
||||
// content: const Text('Are you sure you want to delete this item?'),
|
||||
// actions: [
|
||||
// TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
||||
// TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// if (confirmDelete == true) {
|
||||
// setState(() {
|
||||
// snapshot.comboProducts!.removeAt(index);
|
||||
// });
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// itemBuilder: (BuildContext context) => [
|
||||
// const PopupMenuItem(value: 'edit', child: Text('Edit')),
|
||||
// const PopupMenuItem(
|
||||
// value: 'delete',
|
||||
// child: Text('Delete', style: TextStyle(color: Colors.red)),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
visualDensity: VisualDensity(vertical: -4, horizontal: -4),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
} else
|
||||
Center(child: PermitDenyWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}));
|
||||
}
|
||||
|
||||
// Add stock popup
|
||||
Future<dynamic> addStockPopUp(
|
||||
BuildContext context, GlobalKey<FormState> _formKey, ThemeData theme, Product snapshot, int index) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
insetPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).addStock,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: kTitleColor, size: 16),
|
||||
iconSize: 16,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 30,
|
||||
minHeight: 30,
|
||||
),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(Color(0xffEEF3FF)),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
thickness: 0.3,
|
||||
color: kBorderColorTextField,
|
||||
height: 0,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
textAlign: TextAlign.center,
|
||||
controller: productStockController,
|
||||
validator: (value) {
|
||||
final int? enteredStock = int.tryParse(value ?? '');
|
||||
if (enteredStock == null || enteredStock < 1) {
|
||||
return lang.S.of(context).stockWarn;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
hintText: lang.S.of(context).enterStock,
|
||||
prefixIcon: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
height: 26,
|
||||
width: 26,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color(0xffE0E2E7),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
int quantity = int.tryParse(productStockController.text) ?? 1;
|
||||
if (quantity > 1) {
|
||||
quantity--;
|
||||
productStockController.text = quantity.toString();
|
||||
}
|
||||
},
|
||||
child: Icon(Icons.remove, color: Color(0xff4A4A52)),
|
||||
),
|
||||
),
|
||||
suffixIcon: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
height: 26,
|
||||
width: 26,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: kMainColor.withOpacity(0.15),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
int quantity = int.tryParse(productStockController.text) ?? 1;
|
||||
quantity++;
|
||||
productStockController.text = quantity.toString();
|
||||
},
|
||||
child: Icon(Icons.add, color: theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xffE0E2E7))),
|
||||
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xffE0E2E7))),
|
||||
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xffE0E2E7))),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
controller: salePriceController,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')),
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).salePrice,
|
||||
hintText: lang.S.of(context).enterAmount,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: Color(0xffF68A3D)),
|
||||
),
|
||||
child: Text(lang.S.of(context).cancel, style: TextStyle(color: Color(0xffF68A3D))),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
child: Text(lang.S.of(context).save),
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final int newStock = int.tryParse(productStockController.text) ?? 0;
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: lang.S.of(context).updating);
|
||||
|
||||
final repo = ProductRepo();
|
||||
final String productId = snapshot.stocks?[index].id.toString() ?? '';
|
||||
|
||||
final bool success = await repo.addStock(
|
||||
id: productId,
|
||||
qty: newStock.toString(),
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(lang.S.of(context).updateSuccess)),
|
||||
);
|
||||
|
||||
ref.refresh(fetchProductDetails(widget.details.id.toString()));
|
||||
ref.refresh(productProvider);
|
||||
|
||||
productStockController.clear();
|
||||
salePriceController.clear();
|
||||
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(lang.S.of(context).updateFailed)),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// view modal sheet
|
||||
Future<dynamic> viewModal(BuildContext context, Product snapshot, int index) {
|
||||
final _lang = lang.S.of(context);
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setNewState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).view,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: Icon(Icons.close, size: 18),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(color: kBorderColor, height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (snapshot.stocks != null && snapshot.stocks!.isNotEmpty && index < snapshot.stocks!.length)
|
||||
...{
|
||||
_lang.batchNo: snapshot.stocks![index].batchNo ?? 'n/a',
|
||||
_lang.qty: snapshot.stocks![index].productStock?.toString() ?? '0',
|
||||
_lang.costExclusionTax: snapshot.vatType != 'exclusive'
|
||||
? (snapshot.stocks![index].productPurchasePrice != null && snapshot.vatAmount != null
|
||||
? '${snapshot.stocks![index].productPurchasePrice! - snapshot.vatAmount!}'
|
||||
: 'n/a')
|
||||
: (snapshot.stocks![index].productPurchasePrice?.toString() ?? 'n/a'),
|
||||
_lang.costInclusionTax: snapshot.vatType == 'exclusive'
|
||||
? (snapshot.stocks![index].productPurchasePrice != null && snapshot.vatAmount != null
|
||||
? '${snapshot.stocks![index].productPurchasePrice! + snapshot.vatAmount!}'
|
||||
: 'n/a')
|
||||
: (snapshot.stocks![index].productPurchasePrice?.toString() ?? 'n/a'),
|
||||
'${_lang.profitMargin} (%)': snapshot.stocks![index].profitPercent?.toString() ?? 'n/a',
|
||||
_lang.salePrice: snapshot.stocks![index].productSalePrice?.toString() ?? 'n/a',
|
||||
_lang.wholeSalePrice: snapshot.stocks![index].productWholeSalePrice?.toString() ?? 'n/a',
|
||||
_lang.dealerPrice: snapshot.stocks![index].productDealerPrice?.toString() ?? 'n/a',
|
||||
_lang.manufactureDate:
|
||||
(snapshot.stocks![index].mfgDate != null && snapshot.stocks![index].mfgDate!.isNotEmpty)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.tryParse(snapshot.stocks![index].mfgDate!) ?? DateTime(0))
|
||||
: 'n/a',
|
||||
_lang.expiredDate: (snapshot.stocks![index].expireDate != null &&
|
||||
snapshot.stocks![index].expireDate!.isNotEmpty)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.tryParse(snapshot.stocks![index].expireDate!) ?? DateTime(0))
|
||||
: 'n/a',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(_lang.noStockAvailable),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showEditDeletePopUp(
|
||||
{required BuildContext context, Stock? data, required WidgetRef ref, required String productId}) async {
|
||||
final _theme = Theme.of(context);
|
||||
return await showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).deleteBatchWarn,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 26),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color(0xffF68A3D).withValues(alpha: 0.1),
|
||||
),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: SvgPicture.asset(
|
||||
height: 146,
|
||||
width: 146,
|
||||
'images/trash.svg',
|
||||
),
|
||||
),
|
||||
SizedBox(height: 26),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(lang.S.of(context).cancel),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
await Future.delayed(Duration.zero);
|
||||
ProductRepo repo = ProductRepo();
|
||||
bool success;
|
||||
success = await repo.deleteStock(
|
||||
id: data?.id.toString() ?? '',
|
||||
);
|
||||
if (success) {
|
||||
ref.refresh(fetchProductDetails(productId));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(lang.S.of(context).deletedSuccessFully)));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).delete),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
487
lib/Screens/Products/product_list_screen.dart
Normal file
487
lib/Screens/Products/product_list_screen.dart
Normal file
@@ -0,0 +1,487 @@
|
||||
// File: product_list.dart (Refactored and Cleaned)
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
|
||||
// --- Local Imports ---
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_details.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/provider/setting_provider.dart';
|
||||
import 'package:mobile_pos/Screens/product%20variation/product_variation_list_screen.dart';
|
||||
import 'package:mobile_pos/Screens/product_unit/unit_list.dart';
|
||||
import 'package:mobile_pos/Screens/shelfs/shelf_list_screen.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import '../../GlobalComponents/bar_code_scaner_widget.dart';
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../service/check_actions_when_no_branch.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../barcode/gererate_barcode.dart';
|
||||
import '../product racks/product_racks_list.dart';
|
||||
import '../product_brand/brands_list.dart';
|
||||
import '../product_category/product_category_list_screen.dart';
|
||||
import '../product_model/product_model_list.dart';
|
||||
import '../product_category/provider/product_category_provider/product_unit_provider.dart';
|
||||
import 'Repo/product_repo.dart';
|
||||
import 'add product/add_product.dart';
|
||||
import 'bulk product upload/bulk_product_upload_screen.dart';
|
||||
import '../hrm/widgets/deleteing_alart_dialog.dart';
|
||||
|
||||
class ProductList extends ConsumerStatefulWidget {
|
||||
const ProductList({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductList> createState() => _ProductListState();
|
||||
}
|
||||
|
||||
class _ProductListState extends ConsumerState<ProductList> {
|
||||
bool _isRefreshing = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
final _productRepo = ProductRepo(); // Instantiate repo once
|
||||
|
||||
// --- Data Refresh Logic ---
|
||||
Future<void> _refreshData() async {
|
||||
if (_isRefreshing) return;
|
||||
_isRefreshing = true;
|
||||
|
||||
// Invalidate main providers to force reload
|
||||
ref.invalidate(productProvider);
|
||||
ref.invalidate(categoryProvider);
|
||||
ref.invalidate(fetchSettingProvider);
|
||||
|
||||
// Wait for reload (optional, but good for UX)
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_isRefreshing = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() {
|
||||
setState(() {
|
||||
_searchQuery = _searchController.text;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- Helper Widgets ---
|
||||
|
||||
// Builds the main context menu for product management tasks
|
||||
Widget _buildProductMenu() {
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
// Helper function to build consistent menu items
|
||||
PopupMenuItem<void> buildItem(
|
||||
{required VoidCallback onTap, List<List<dynamic>>? hugeIcons, IconData? icon, required String text}) {
|
||||
return PopupMenuItem(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
hugeIcons != null
|
||||
? HugeIcon(
|
||||
icon: hugeIcons,
|
||||
color: kPeraColor,
|
||||
size: 20,
|
||||
)
|
||||
: Icon(
|
||||
icon,
|
||||
color: kPeraColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return PopupMenuButton<void>(
|
||||
itemBuilder: (context) => [
|
||||
buildItem(
|
||||
onTap: () => const CategoryList(isFromProductList: true).launch(context),
|
||||
hugeIcons: HugeIcons.strokeRoundedAddToList,
|
||||
text: lang.S.of(context).productCategory,
|
||||
),
|
||||
buildItem(
|
||||
onTap: () => const BrandsList(isFromProductList: true).launch(context),
|
||||
hugeIcons: HugeIcons.strokeRoundedSecurityCheck,
|
||||
text: lang.S.of(context).brand,
|
||||
),
|
||||
buildItem(
|
||||
onTap: () => const ProductModelList(fromProductList: true).launch(context),
|
||||
hugeIcons: HugeIcons.strokeRoundedDrawingMode,
|
||||
text: lang.S.of(context).model,
|
||||
),
|
||||
buildItem(
|
||||
onTap: () => const UnitList(isFromProductList: true).launch(context),
|
||||
hugeIcons: HugeIcons.strokeRoundedCells,
|
||||
text: lang.S.of(context).productUnit,
|
||||
),
|
||||
buildItem(
|
||||
onTap: () => const ProductShelfList(isFromProductList: true).launch(context),
|
||||
icon: Bootstrap.bookshelf,
|
||||
text: lang.S.of(context).shelf,
|
||||
),
|
||||
buildItem(
|
||||
onTap: () => const ProductRackList(isFromProductList: true).launch(context),
|
||||
icon: Bootstrap.hdd_rack,
|
||||
text: lang.S.of(context).racks,
|
||||
),
|
||||
buildItem(
|
||||
onTap: () => const ProductVariationList().launch(context),
|
||||
hugeIcons: HugeIcons.strokeRoundedPackage,
|
||||
text: lang.S.of(context).variations,
|
||||
),
|
||||
// const PopupMenuDivider(),
|
||||
buildItem(
|
||||
onTap: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (result) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const BulkUploader(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
hugeIcons: HugeIcons.strokeRoundedInboxUpload,
|
||||
text: lang.S.of(context).bulk,
|
||||
),
|
||||
buildItem(
|
||||
onTap: () => const BarcodeGeneratorScreen().launch(context),
|
||||
hugeIcons: HugeIcons.strokeRoundedBarCode01,
|
||||
text: lang.S.of(context).barcodeGen,
|
||||
),
|
||||
],
|
||||
offset: const Offset(0, 40),
|
||||
color: kWhite,
|
||||
padding: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
);
|
||||
}
|
||||
|
||||
// Builds the search and barcode scanner input field
|
||||
Widget _buildSearchInput() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: lang.S.of(context).searchH,
|
||||
prefixIcon: Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16),
|
||||
child: Icon(
|
||||
AntDesign.search_outline,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsetsDirectional.zero,
|
||||
|
||||
),
|
||||
onChanged: (value) {
|
||||
// No need for setState here as controller listener already handles it
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor50,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => BarcodeScannerWidget(
|
||||
onBarcodeFound: (String code) {
|
||||
setState(() {
|
||||
_searchController.text = code;
|
||||
_searchQuery = code;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedIrisScan,
|
||||
color: kMainColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Builds the main list of products
|
||||
Widget _buildProductList(List<dynamic> products) {
|
||||
final filteredProducts = products.where((product) {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
final name = product.productName?.toLowerCase() ?? '';
|
||||
final code = product.productCode?.toLowerCase() ?? '';
|
||||
return name.contains(query) || code.contains(query);
|
||||
}).toList();
|
||||
|
||||
final permissionService = PermissionService(ref);
|
||||
final _theme = Theme.of(context);
|
||||
final locale = Localizations.localeOf(context).languageCode;
|
||||
|
||||
if (!permissionService.hasPermission(Permit.productsRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
|
||||
if (filteredProducts.isEmpty && _searchQuery.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 30.0),
|
||||
child: Text(
|
||||
lang.S.of(context).addProduct,
|
||||
maxLines: 2,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredProducts.isEmpty && _searchQuery.isNotEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 30.0),
|
||||
child: Text(lang.S.of(context).noProductMatchYourSearch),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredProducts.length,
|
||||
itemBuilder: (_, i) {
|
||||
final product = filteredProducts[i];
|
||||
|
||||
// Helper function for building PopupMenuItems (Edit/Delete)
|
||||
PopupMenuItem<int> buildActionItem(
|
||||
{required int value, required IconData icon, required String text, required VoidCallback onTap}) {
|
||||
return PopupMenuItem(
|
||||
onTap: onTap,
|
||||
value: value,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: kGreyTextColor),
|
||||
const SizedBox(width: 10),
|
||||
Text(text, style: _theme.textTheme.bodyMedium?.copyWith(color: kGreyTextColor)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
onTap: () =>
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => ProductDetails(details: product))),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: product.productPicture == null
|
||||
? CircleAvatarWidget(
|
||||
name: product.productName,
|
||||
size: const Size(50, 50),
|
||||
)
|
||||
: Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: NetworkImage('${APIConfig.domain}${product.productPicture!}'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
product.productName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
product.productType == 'combo'
|
||||
? '$currency${product.productSalePrice.toString()}'
|
||||
: "$currency${product.stocks != null && product.stocks!.isNotEmpty && product.stocks!.first.productSalePrice != null ? product.stocks!.first.productSalePrice : '0'}",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${lang.S.of(context).type} : ${product.productType == 'single' ? lang.S.of(context).single : product.productType == 'variant' ? locale == 'en' ? 'Variant' : lang.S.of(context).variations : product.productType == 'combo' ? lang.S.of(context).combo : product.productType}",
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '${lang.S.of(context).stock} : ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: product.productType == 'combo'
|
||||
? lang.S.of(context).combo
|
||||
: '${product.stocksSumProductStock ?? '0'}',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: DAppColors.kSuccess,
|
||||
))
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<int>(
|
||||
style: const ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.zero)),
|
||||
itemBuilder: (context) => [
|
||||
// Edit Action
|
||||
buildActionItem(
|
||||
value: 1,
|
||||
icon: IconlyBold.edit,
|
||||
text: lang.S.of(context).edit,
|
||||
onTap: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (!result) return;
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => AddProduct(productModel: product)));
|
||||
},
|
||||
),
|
||||
// Delete Action
|
||||
buildActionItem(
|
||||
value: 2,
|
||||
icon: IconlyBold.delete,
|
||||
text: lang.S.of(context).delete,
|
||||
onTap: () async {
|
||||
bool confirmDelete = await showDeleteConfirmationDialog(context: context, itemName: 'product');
|
||||
if (confirmDelete) {
|
||||
EasyLoading.show(status: lang.S.of(context).deleting);
|
||||
await _productRepo.deleteProduct(id: product.id.toString(), context: context, ref: ref);
|
||||
// Refresh will happen automatically if repo is implemented correctly
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
offset: const Offset(0, 40),
|
||||
color: kWhite,
|
||||
padding: EdgeInsets.zero,
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return Divider(color: const Color(0xff808191).withAlpha(50));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- Main Build Method ---
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
surfaceTintColor: kWhite,
|
||||
elevation: 0,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
title: Text(lang.S.of(context).productList),
|
||||
actions: [_buildProductMenu()],
|
||||
toolbarHeight: 80,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(40),
|
||||
child: _buildSearchInput(),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
backgroundColor: kMainColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)),
|
||||
onPressed: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (result) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => AddProduct()));
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.add, color: kWhite),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: Consumer(builder: (context, ref, __) {
|
||||
final providerData = ref.watch(productProvider);
|
||||
// This is the outer check, but the main data display is inside providerData.when
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
|
||||
return businessInfo.when(
|
||||
data: (_) => providerData.when(
|
||||
data: (products) => SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: _buildProductList(products),
|
||||
),
|
||||
error: (e, stack) => Center(child: Text('Error loading products: ${e.toString()}')),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (e, stack) => Center(child: Text('Error loading business info: ${e.toString()}')),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
class GetProductSettingModel {
|
||||
GetProductSettingModel({
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
GetProductSettingModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
data = json['data'] != null ? Data.fromJson(json['data']) : null;
|
||||
}
|
||||
String? message;
|
||||
Data? data;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['message'] = message;
|
||||
if (data != null) {
|
||||
map['data'] = data?.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Data {
|
||||
Data({
|
||||
// this.id,
|
||||
// this.businessId,
|
||||
this.modules,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
Data.fromJson(dynamic json) {
|
||||
// id = json['id'];
|
||||
// businessId = json['business_id'];
|
||||
modules = json['modules'] != null ? Modules.fromJson(json['modules']) : null;
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
// num? id;
|
||||
// num? businessId;
|
||||
Modules? modules;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
// map['id'] = id;
|
||||
// map['business_id'] = businessId;
|
||||
if (modules != null) {
|
||||
map['modules'] = modules?.toJson();
|
||||
}
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Modules {
|
||||
Modules({
|
||||
this.showProductName,
|
||||
this.variantName,
|
||||
this.showProductCode,
|
||||
this.showProductStock,
|
||||
this.showProductSalePrice,
|
||||
this.showProductDealerPrice,
|
||||
this.showProductWholesalePrice,
|
||||
this.showProductUnit,
|
||||
this.showProductBrand,
|
||||
this.showProductCategory,
|
||||
this.showProductManufacturer,
|
||||
this.showProductImage,
|
||||
this.showExpireDate,
|
||||
this.showAlertQty,
|
||||
this.showVatId,
|
||||
this.showVatType,
|
||||
this.showExclusivePrice,
|
||||
this.showInclusivePrice,
|
||||
this.showProfitPercent,
|
||||
this.showWarehouse,
|
||||
this.showBatchNo,
|
||||
this.showMfgDate,
|
||||
this.showModelNo,
|
||||
this.defaultSalePrice,
|
||||
this.defaultWholesalePrice,
|
||||
this.defaultDealerPrice,
|
||||
this.showProductTypeSingle,
|
||||
this.showProductTypeVariant,
|
||||
this.showAction,
|
||||
this.defaultExpiredDate,
|
||||
this.defaultMfgDate,
|
||||
this.expireDateType,
|
||||
this.mfgDateType,
|
||||
this.showProductBatchNo,
|
||||
this.showProductExpireDate,
|
||||
// --- NEW FIELDS ---
|
||||
this.showProductTypeCombo,
|
||||
this.showRack,
|
||||
this.showShelf,
|
||||
this.showGuaranty,
|
||||
this.showWarranty,
|
||||
// this.showSerial,
|
||||
// ------------------
|
||||
});
|
||||
|
||||
Modules.fromJson(dynamic json) {
|
||||
showProductName = json['show_product_name'];
|
||||
variantName = json['variant_name'];
|
||||
showProductCode = json['show_product_code'];
|
||||
showProductStock = json['show_product_stock'];
|
||||
showProductSalePrice = json['show_product_sale_price'];
|
||||
showProductDealerPrice = json['show_product_dealer_price'];
|
||||
showProductWholesalePrice = json['show_product_wholesale_price'];
|
||||
showProductUnit = json['show_product_unit'];
|
||||
showProductBrand = json['show_product_brand'];
|
||||
showProductCategory = json['show_product_category'];
|
||||
showProductManufacturer = json['show_product_manufacturer'];
|
||||
showProductImage = json['show_product_image'];
|
||||
showExpireDate = json['show_expire_date'];
|
||||
showAlertQty = json['show_alert_qty'];
|
||||
showVatId = json['show_vat_id'];
|
||||
showVatType = json['show_vat_type'];
|
||||
showWarehouse = json['show_warehouse'];
|
||||
showExclusivePrice = json['show_exclusive_price'];
|
||||
showInclusivePrice = json['show_inclusive_price'];
|
||||
showProfitPercent = json['show_profit_percent'];
|
||||
showBatchNo = json['show_batch_no'];
|
||||
showMfgDate = json['show_mfg_date'];
|
||||
showModelNo = json['show_model_no'];
|
||||
defaultSalePrice = json['default_sale_price'];
|
||||
defaultWholesalePrice = json['default_wholesale_price'];
|
||||
defaultDealerPrice = json['default_dealer_price'];
|
||||
showProductTypeSingle = json['show_product_type_single'];
|
||||
showProductTypeVariant = json['show_product_type_variant'];
|
||||
showAction = json['show_action'];
|
||||
defaultExpiredDate = json['default_expired_date'];
|
||||
defaultMfgDate = json['default_mfg_date'];
|
||||
expireDateType = json['expire_date_type'];
|
||||
mfgDateType = json['mfg_date_type'];
|
||||
showProductBatchNo = json['show_product_batch_no'];
|
||||
showProductExpireDate = json['show_product_expire_date'];
|
||||
// --- NEW FIELDS ---
|
||||
showProductTypeCombo = json['show_product_type_combo'] ?? '1';
|
||||
showRack = json['show_rack'] ?? '1';
|
||||
showShelf = json['show_shelf'] ?? '1';
|
||||
showGuaranty = json['show_guarantee'] ?? '1';
|
||||
showWarranty = json['show_warranty'] ?? '1';
|
||||
// showSerial = json['show_serial'] ?? '1';
|
||||
// ------------------
|
||||
}
|
||||
String? showProductName;
|
||||
String? showProductCode;
|
||||
String? showProductStock;
|
||||
String? showProductSalePrice;
|
||||
String? showProductDealerPrice;
|
||||
String? showProductWholesalePrice;
|
||||
String? showProductUnit;
|
||||
String? showProductBrand;
|
||||
String? showProductCategory;
|
||||
String? showProductManufacturer;
|
||||
String? showProductImage;
|
||||
String? showExpireDate;
|
||||
String? showAlertQty;
|
||||
String? showVatId;
|
||||
String? showVatType;
|
||||
String? showExclusivePrice;
|
||||
String? showInclusivePrice;
|
||||
String? showProfitPercent;
|
||||
String? showBatchNo;
|
||||
String? variantName;
|
||||
String? showMfgDate;
|
||||
String? showModelNo;
|
||||
String? defaultSalePrice;
|
||||
String? defaultWholesalePrice;
|
||||
String? defaultDealerPrice;
|
||||
String? showProductTypeSingle;
|
||||
String? showProductTypeVariant;
|
||||
String? showAction;
|
||||
String? defaultExpiredDate;
|
||||
String? defaultMfgDate;
|
||||
String? expireDateType;
|
||||
String? mfgDateType;
|
||||
String? showProductBatchNo;
|
||||
String? showProductExpireDate;
|
||||
// --- NEW FIELDS ---
|
||||
String? showWarehouse;
|
||||
String? showProductTypeCombo;
|
||||
String? showRack;
|
||||
String? showShelf;
|
||||
String? showGuaranty;
|
||||
String? showWarranty;
|
||||
// String? showSerial;
|
||||
|
||||
// ------------------
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['show_product_name'] = showProductName;
|
||||
map['variant_name'] = variantName;
|
||||
map['show_product_code'] = showProductCode;
|
||||
map['show_product_stock'] = showProductStock;
|
||||
map['show_product_sale_price'] = showProductSalePrice;
|
||||
map['show_product_dealer_price'] = showProductDealerPrice;
|
||||
map['show_product_wholesale_price'] = showProductWholesalePrice;
|
||||
map['show_product_unit'] = showProductUnit;
|
||||
map['show_product_brand'] = showProductBrand;
|
||||
map['show_product_category'] = showProductCategory;
|
||||
map['show_product_manufacturer'] = showProductManufacturer;
|
||||
map['show_product_image'] = showProductImage;
|
||||
map['show_expire_date'] = showExpireDate;
|
||||
map['show_alert_qty'] = showAlertQty;
|
||||
map['show_vat_id'] = showVatId;
|
||||
map['show_warehouse'] = showWarehouse;
|
||||
map['show_vat_type'] = showVatType;
|
||||
map['show_exclusive_price'] = showExclusivePrice;
|
||||
map['show_inclusive_price'] = showInclusivePrice;
|
||||
map['show_profit_percent'] = showProfitPercent;
|
||||
map['show_batch_no'] = showBatchNo;
|
||||
map['show_mfg_date'] = showMfgDate;
|
||||
map['show_model_no'] = showModelNo;
|
||||
map['default_sale_price'] = defaultSalePrice;
|
||||
map['default_wholesale_price'] = defaultWholesalePrice;
|
||||
map['default_dealer_price'] = defaultDealerPrice;
|
||||
map['show_product_type_single'] = showProductTypeSingle;
|
||||
map['show_product_type_variant'] = showProductTypeVariant;
|
||||
map['show_action'] = showAction;
|
||||
map['default_expired_date'] = defaultExpiredDate;
|
||||
map['default_mfg_date'] = defaultMfgDate;
|
||||
map['expire_date_type'] = expireDateType;
|
||||
map['mfg_date_type'] = mfgDateType;
|
||||
map['show_product_batch_no'] = showProductBatchNo;
|
||||
map['show_product_expire_date'] = showProductExpireDate;
|
||||
// --- NEW FIELDS ---
|
||||
map['show_product_type_combo'] = showProductTypeCombo;
|
||||
map['show_rack'] = showRack;
|
||||
map['show_shelf'] = showShelf;
|
||||
map['show_guarantee'] = showGuaranty;
|
||||
map['show_warranty'] = showWarranty;
|
||||
// map['show_serial'] = showSerial;
|
||||
// ------------------
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
class UpdateProductSettingModel {
|
||||
String? productCode;
|
||||
String? productStock;
|
||||
String? salePrice;
|
||||
String? dealerPrice;
|
||||
String? wholesalePrice;
|
||||
String? unit;
|
||||
String? brand;
|
||||
String? category;
|
||||
String? manufacturer;
|
||||
String? image;
|
||||
String? showExpireDate;
|
||||
String? alertQty;
|
||||
String? vatId;
|
||||
String? vatType;
|
||||
String? exclusivePrice;
|
||||
String? inclusivePrice;
|
||||
String? profitPercent;
|
||||
String? batchNo;
|
||||
String? showManufactureDate;
|
||||
String? model;
|
||||
String? showSingle;
|
||||
String? showVariant;
|
||||
String? showAction;
|
||||
String? defaultExpireDate;
|
||||
String? defaultManufactureDate;
|
||||
String? expireDateType;
|
||||
String? manufactureDateType;
|
||||
String? showBatchNo;
|
||||
// --- NEW FIELDS ---
|
||||
String? showWarehouse;
|
||||
String? showProductTypeCombo;
|
||||
String? showRack;
|
||||
String? showShelf;
|
||||
String? showGuaranty;
|
||||
String? showWarranty;
|
||||
// String? showSerial;
|
||||
|
||||
// String? defaultSalePrice;
|
||||
// String? defaultWholeSalePrice;
|
||||
// String? defaultDealerPrice;
|
||||
|
||||
UpdateProductSettingModel({
|
||||
this.productCode,
|
||||
this.productStock,
|
||||
this.salePrice,
|
||||
this.dealerPrice,
|
||||
this.wholesalePrice,
|
||||
this.unit,
|
||||
this.brand,
|
||||
this.category,
|
||||
this.manufacturer,
|
||||
this.image,
|
||||
this.showExpireDate,
|
||||
this.alertQty,
|
||||
this.vatId,
|
||||
this.vatType,
|
||||
this.exclusivePrice,
|
||||
this.inclusivePrice,
|
||||
this.profitPercent,
|
||||
this.batchNo,
|
||||
this.showManufactureDate,
|
||||
this.model,
|
||||
this.showSingle,
|
||||
this.showVariant,
|
||||
this.showAction,
|
||||
this.defaultExpireDate,
|
||||
this.defaultManufactureDate,
|
||||
this.expireDateType,
|
||||
this.manufactureDateType,
|
||||
this.showBatchNo,
|
||||
this.showWarranty,
|
||||
this.showGuaranty,
|
||||
this.showShelf,
|
||||
this.showRack,
|
||||
this.showProductTypeCombo,
|
||||
this.showWarehouse,
|
||||
// this.showSerial,
|
||||
// --- NEW FIELDS ---
|
||||
// this.defaultSalePrice,
|
||||
// this.defaultWholeSalePrice,
|
||||
// this.defaultDealerPrice,
|
||||
});
|
||||
}
|
||||
305
lib/Screens/Products/product_setting/product_setting_drawer.dart
Normal file
305
lib/Screens/Products/product_setting/product_setting_drawer.dart
Normal file
@@ -0,0 +1,305 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/provider/setting_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/repo/product_setting_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
import 'model/get_product_setting_model.dart';
|
||||
import 'model/product_setting_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class ProductSettingsDrawer extends ConsumerStatefulWidget {
|
||||
final VoidCallback? onSave;
|
||||
final Modules? modules;
|
||||
|
||||
const ProductSettingsDrawer({
|
||||
super.key,
|
||||
this.onSave,
|
||||
this.modules,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductSettingsDrawer> createState() => _ProductSettingsDrawerState();
|
||||
}
|
||||
|
||||
class _ProductSettingsDrawerState extends ConsumerState<ProductSettingsDrawer> {
|
||||
final Map<String, bool> _switchValues = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final modules = widget.modules;
|
||||
|
||||
_switchValues.addAll({
|
||||
'Product Code': modules?.showProductCode == '1',
|
||||
'Product Stock': modules?.showProductStock == '1',
|
||||
'Sale': modules?.showProductSalePrice == '1',
|
||||
'Dealer': modules?.showProductDealerPrice == '1',
|
||||
'Wholesale Price': modules?.showProductWholesalePrice == '1',
|
||||
'Unit': modules?.showProductUnit == '1',
|
||||
'Brand': modules?.showProductBrand == '1',
|
||||
'Category': modules?.showProductCategory == '1',
|
||||
'Manufacturer': modules?.showProductManufacturer == '1',
|
||||
'Image': modules?.showProductImage == '1',
|
||||
'Show Expire Date': modules?.showExpireDate == '1',
|
||||
'Low Stock Alert': modules?.showAlertQty == '1',
|
||||
'Vat Id': modules?.showVatId == '1',
|
||||
'Vat Type': modules?.showVatType == '1',
|
||||
'Exclusive Price': modules?.showExclusivePrice == '1',
|
||||
'Inclusive Price': modules?.showInclusivePrice == '1',
|
||||
'Profit Percent': modules?.showProfitPercent == '1',
|
||||
'Batch No': modules?.showBatchNo == '1',
|
||||
'Show Manufacture Date': modules?.showMfgDate == '1',
|
||||
'Model': modules?.showModelNo == '1',
|
||||
'Show Single': modules?.showProductTypeSingle == '1',
|
||||
'Show Combo': modules?.showProductTypeCombo == '1',
|
||||
'Show Variant': modules?.showProductTypeVariant == '1',
|
||||
'Show Action': modules?.showAction == '1',
|
||||
'Warehouse': modules?.showWarehouse == '1',
|
||||
'Rack': modules?.showRack == '1',
|
||||
'Shelf': modules?.showShelf == '1',
|
||||
'Guarantee': modules?.showGuaranty == '1',
|
||||
'Warranty': modules?.showWarranty == '1',
|
||||
});
|
||||
|
||||
_saleController.text = modules?.defaultSalePrice ?? '';
|
||||
_wholesaleController.text = modules?.defaultWholesalePrice ?? '';
|
||||
_dealerController.text = modules?.defaultDealerPrice ?? '';
|
||||
}
|
||||
|
||||
final TextEditingController _saleController = TextEditingController();
|
||||
final TextEditingController _wholesaleController = TextEditingController();
|
||||
final TextEditingController _dealerController = TextEditingController();
|
||||
GlobalKey<FormState> globalKey = GlobalKey<FormState>();
|
||||
String getStringFromBool(Map<String, bool> map, String key) {
|
||||
return map[key] == true ? '1' : '0';
|
||||
}
|
||||
|
||||
final Map<String, String Function(lang.S)> labelMap = {
|
||||
'Product Code': (s) => s.productCode,
|
||||
'Product Stock': (s) => s.productStock,
|
||||
'Sale': (s) => s.salePrice,
|
||||
'Dealer': (s) => s.dealerPrice,
|
||||
'Wholesale Price': (s) => s.wholeSalePrice,
|
||||
'Unit': (s) => s.unit,
|
||||
'Brand': (s) => s.brand,
|
||||
'Category': (s) => s.category,
|
||||
'Manufacturer': (s) => s.manufacturer,
|
||||
'Image': (s) => s.image,
|
||||
'Show Expire Date': (s) => s.showExpireDate,
|
||||
'Low Stock Alert': (s) => s.lowStockAlert,
|
||||
'Vat Id': (s) => s.vatId,
|
||||
'Vat Type': (s) => s.vatType,
|
||||
'Exclusive Price': (s) => s.exclusivePrice,
|
||||
'Inclusive Price': (s) => s.inclusivePrice,
|
||||
'Profit Percent': (s) => s.profitPercent,
|
||||
'Batch No': (s) => s.batchNo,
|
||||
'Show Manufacture Date': (s) => s.manufactureDate,
|
||||
'Model': (s) => s.model,
|
||||
'Show Single': (s) => s.showSingle,
|
||||
'Show Combo': (s) => s.showCombo,
|
||||
'Show Variant': (s) => s.showVariant,
|
||||
'Show Action': (s) => s.showAction,
|
||||
'Warehouse': (s) => s.warehouse,
|
||||
'Rack': (s) => s.rack,
|
||||
'Shelf': (s) => s.shelf,
|
||||
'Guarantee': (s) => s.guarantee,
|
||||
'Warranty': (s) => s.warranty,
|
||||
};
|
||||
|
||||
String _label(BuildContext context, String key) {
|
||||
final s = lang.S.of(context);
|
||||
return labelMap[key]?.call(s) ?? key;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Drawer(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: globalKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 4, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).productSetting,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(color: kTitleColor, fontWeight: FontWeight.w600, fontSize: 16),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(color: Color(0xffE6E6E6)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Switches List
|
||||
..._switchValues.entries.map(_buildSwitchTile),
|
||||
Divider(),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Price Fields
|
||||
// Text('PRICE SETTINGS', style: Theme.of(context).textTheme.bodyMedium),
|
||||
// SizedBox(height: 14),
|
||||
// _buildPriceField('Sale Price', _saleController),
|
||||
// SizedBox(height: 8),
|
||||
// _buildPriceField('Wholesale Price', _wholesaleController),
|
||||
// SizedBox(height: 8),
|
||||
// _buildPriceField('Dealer Price', _dealerController),
|
||||
|
||||
// SizedBox(height: 16),
|
||||
|
||||
// Save Button
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
if ((globalKey.currentState?.validate() ?? false)) {
|
||||
final single = _switchValues['Show Single'] ?? false;
|
||||
final variant = _switchValues['Show Variant'] ?? false;
|
||||
final combo = _switchValues['Show Combo'] ?? false;
|
||||
|
||||
if (!single && !variant && !combo) {
|
||||
EasyLoading.showError('Please enable at least one: Single, Variant or Combo');
|
||||
return;
|
||||
}
|
||||
ProductSettingRepo setting = ProductSettingRepo();
|
||||
bool success;
|
||||
// Prepare the data for the update
|
||||
UpdateProductSettingModel data = UpdateProductSettingModel(
|
||||
productCode: getStringFromBool(_switchValues, 'Product Code'),
|
||||
productStock: getStringFromBool(_switchValues, 'Product Stock'),
|
||||
salePrice: getStringFromBool(_switchValues, 'Sale'),
|
||||
dealerPrice: getStringFromBool(_switchValues, 'Dealer'),
|
||||
wholesalePrice: getStringFromBool(_switchValues, 'Wholesale Price'),
|
||||
unit: getStringFromBool(_switchValues, 'Unit'),
|
||||
brand: getStringFromBool(_switchValues, 'Brand'),
|
||||
category: getStringFromBool(_switchValues, 'Category'),
|
||||
manufacturer: getStringFromBool(_switchValues, 'Manufacturer'),
|
||||
image: getStringFromBool(_switchValues, 'Image'),
|
||||
showExpireDate: getStringFromBool(_switchValues, 'Show Expire Date'),
|
||||
alertQty: getStringFromBool(_switchValues, 'Low Stock Alert'),
|
||||
vatId: getStringFromBool(_switchValues, 'Vat Id'),
|
||||
vatType: getStringFromBool(_switchValues, 'Vat Type'),
|
||||
exclusivePrice: getStringFromBool(_switchValues, 'Exclusive Price'),
|
||||
inclusivePrice: getStringFromBool(_switchValues, 'Inclusive Price'),
|
||||
profitPercent: getStringFromBool(_switchValues, 'Profit Percent'),
|
||||
batchNo: getStringFromBool(_switchValues, 'Batch No'),
|
||||
showManufactureDate: getStringFromBool(_switchValues, 'Show Manufacture Date'),
|
||||
model: getStringFromBool(_switchValues, 'Model'),
|
||||
showWarehouse: getStringFromBool(_switchValues, 'Warehouse'),
|
||||
showRack: getStringFromBool(_switchValues, 'Rack'),
|
||||
showShelf: getStringFromBool(_switchValues, 'Shelf'),
|
||||
showSingle: getStringFromBool(_switchValues, 'Show Single'),
|
||||
showProductTypeCombo: getStringFromBool(_switchValues, 'Show Combo'),
|
||||
showVariant: getStringFromBool(_switchValues, 'Show Variant'),
|
||||
showAction: getStringFromBool(_switchValues, 'Show Action'),
|
||||
defaultExpireDate: getStringFromBool(_switchValues, 'Default ExpireDate'),
|
||||
defaultManufactureDate:
|
||||
getStringFromBool(_switchValues, 'Default Manufacture Date'),
|
||||
expireDateType: getStringFromBool(_switchValues, 'ExpireDate type'),
|
||||
manufactureDateType: getStringFromBool(_switchValues, 'ManufactureDate type'),
|
||||
showBatchNo: getStringFromBool(_switchValues, 'Show batch no.'),
|
||||
showWarranty: getStringFromBool(_switchValues, 'Warranty'),
|
||||
showGuaranty: getStringFromBool(_switchValues, 'Guarantee'),
|
||||
// defaultSalePrice: _saleController.text,
|
||||
// defaultDealerPrice: _dealerController.text,
|
||||
// defaultWholeSalePrice: _wholesaleController.text,
|
||||
);
|
||||
success = await setting.updateProductSetting(data: data);
|
||||
if (success) {
|
||||
EasyLoading.showSuccess('Update Successfully');
|
||||
ref.refresh(fetchSettingProvider);
|
||||
widget.onSave?.call();
|
||||
} else {
|
||||
EasyLoading.showError('Please Try Again!');
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(),
|
||||
child: Text(lang.S.of(context).saveSetting),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchTile(MapEntry<String, bool> entry) {
|
||||
return ListTile(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||
title: Text(_label(context, entry.key)),
|
||||
trailing: Transform.scale(
|
||||
scale: 0.7,
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: CupertinoSwitch(
|
||||
applyTheme: true,
|
||||
value: entry.value,
|
||||
onChanged: (value) => setState(() => _switchValues[entry.key] = value),
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
inactiveTrackColor: Color(0xff999999),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceField(String label, TextEditingController controller) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_saleController.dispose();
|
||||
_wholesaleController.dispose();
|
||||
_dealerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../model/get_product_setting_model.dart';
|
||||
import '../repo/product_setting_repo.dart';
|
||||
|
||||
ProductSettingRepo repo = ProductSettingRepo();
|
||||
|
||||
final fetchSettingProvider = FutureProvider<GetProductSettingModel>((ref) {
|
||||
return repo.fetchProductSetting();
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../Repository/constant_functions.dart';
|
||||
import '../../../../http_client/customer_http_client_get.dart';
|
||||
import '../model/get_product_setting_model.dart';
|
||||
import '../model/product_setting_model.dart';
|
||||
|
||||
class ProductSettingRepo {
|
||||
// add/update setting
|
||||
Future<bool> updateProductSetting({required UpdateProductSettingModel data}) async {
|
||||
EasyLoading.show(status: 'Updating');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final url = Uri.parse('${APIConfig.url}/product-settings');
|
||||
|
||||
var request = http.MultipartRequest('POST', url);
|
||||
request.headers.addAll({
|
||||
'Accept': 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
});
|
||||
|
||||
request.fields['show_product_name'] = '1';
|
||||
request.fields['show_product_code'] = data.productCode.toString();
|
||||
request.fields['show_product_stock'] = data.productStock.toString();
|
||||
request.fields['show_product_sale_price'] = data.salePrice.toString();
|
||||
request.fields['show_product_dealer_price'] = data.dealerPrice.toString();
|
||||
request.fields['show_product_wholesale_price'] = data.wholesalePrice.toString();
|
||||
request.fields['show_product_unit'] = data.unit.toString();
|
||||
request.fields['show_product_brand'] = data.brand.toString();
|
||||
request.fields['show_product_category'] = data.category.toString();
|
||||
request.fields['show_product_manufacturer'] = data.manufacturer.toString();
|
||||
request.fields['show_product_image'] = data.image.toString();
|
||||
request.fields['show_expire_date'] = data.showExpireDate.toString();
|
||||
request.fields['show_alert_qty'] = data.alertQty.toString();
|
||||
request.fields['show_vat_id'] = data.vatId.toString();
|
||||
request.fields['show_vat_type'] = data.vatType.toString();
|
||||
request.fields['show_exclusive_price'] = data.exclusivePrice.toString();
|
||||
request.fields['show_inclusive_price'] = data.inclusivePrice.toString();
|
||||
request.fields['show_profit_percent'] = data.profitPercent.toString();
|
||||
request.fields['show_batch_no'] = data.batchNo.toString();
|
||||
request.fields['show_mfg_date'] = data.showManufactureDate.toString();
|
||||
request.fields['show_model_no'] = data.model.toString();
|
||||
request.fields['show_product_type_single'] = data.showSingle.toString();
|
||||
request.fields['show_product_type_variant'] = data.showVariant.toString();
|
||||
request.fields['show_action'] = data.showAction.toString();
|
||||
request.fields['default_expired_date'] = data.defaultExpireDate.toString();
|
||||
request.fields['default_mfg_date'] = data.defaultManufactureDate.toString();
|
||||
request.fields['expire_date_type'] = data.expireDateType.toString();
|
||||
request.fields['mfg_date_type'] = data.manufactureDateType.toString();
|
||||
request.fields['mfg_date_type'] = data.manufactureDateType.toString();
|
||||
request.fields['show_product_type_combo'] = data.showProductTypeCombo.toString();
|
||||
request.fields['show_warehouse'] = data.showWarehouse.toString();
|
||||
request.fields['show_rack'] = data.showRack.toString();
|
||||
request.fields['show_shelf'] = data.showShelf.toString();
|
||||
request.fields['show_guarantee'] = data.showGuaranty.toString();
|
||||
request.fields['show_warranty'] = data.showWarranty.toString();
|
||||
|
||||
try {
|
||||
var response = await request.send();
|
||||
|
||||
var responseData = await http.Response.fromStream(response);
|
||||
EasyLoading.dismiss();
|
||||
print(response.statusCode);
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
var data = jsonDecode(responseData.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Failed to update');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<GetProductSettingModel> fetchProductSetting() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final url = Uri.parse('${APIConfig.url}/product-settings');
|
||||
|
||||
try {
|
||||
var response = await clientGet.get(url: url);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
return GetProductSettingModel.fromJson(jsonData);
|
||||
} else {
|
||||
var data = jsonDecode(response.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Failed to Setting');
|
||||
throw Exception(data['message'] ?? 'Failed to fetch Setting');
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
throw Exception('Error: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user