import 'package:equatable/equatable.dart'; import '../../core/constants/app_constants.dart'; /// Merchandise categories shown as filter chips on the POS dashboard. enum ProductCategory { dairy('Dairy', '๐Ÿฅ›'), grocery('Grocery', '๐Ÿ›’'), fruits('Fruits', '๐ŸŽ'), vegetables('Vegetables', '๐Ÿฅฆ'), beverages('Beverages', '๐Ÿฅค'), snacks('Snacks', '๐Ÿช'), personalCare('Personal Care', '๐Ÿงด'), household('Household', '๐Ÿ '); const ProductCategory(this.label, this.emoji); final String label; final String emoji; } /// Unit of measure โ€” drives whether fractional quantities are permitted. enum UnitOfMeasure { piece('pc'), kilogram('kg'), gram('g'), litre('L'), millilitre('ml'), pack('pack'); const UnitOfMeasure(this.symbol); final String symbol; bool get allowsFractional => this == UnitOfMeasure.kilogram || this == UnitOfMeasure.litre; } /// A sellable item in the catalogue. class Product extends Equatable { const Product({ required this.id, required this.name, required this.barcode, required this.sku, required this.category, required this.price, required this.stock, this.mrp, this.emoji = '๐Ÿ“ฆ', this.imageUrl, this.unit = UnitOfMeasure.piece, this.gstRate = AppConstants.defaultGstRate, this.hsnCode, this.brand, this.isActive = true, }); final String id; final String name; final String barcode; final String sku; final ProductCategory category; /// Selling price per [unit], inclusive of GST (Indian retail convention). final double price; /// Printed maximum retail price, used to display savings. final double? mrp; final double stock; final String emoji; final String? imageUrl; final UnitOfMeasure unit; final double gstRate; /// HSN/SAC code. Printed on the tax invoice โ€” legally required on a GST /// invoice above the turnover threshold. Blank when the catalogue omits it. final String? hsnCode; final String? brand; final bool isActive; bool get isOutOfStock => stock <= 0; bool get isLowStock => stock > 0 && stock <= AppConstants.lowStockThreshold; bool get hasDiscount => mrp != null && mrp! > price; double get savings => hasDiscount ? (mrp! - price) : 0; double get discountPercent => hasDiscount ? ((mrp! - price) / mrp!) * 100 : 0; /// Price stripped of embedded GST โ€” the taxable value. double get netPrice => price / (1 + gstRate); /// GST rupees embedded inside [price]. double get taxPerUnit => price - netPrice; /// Fuzzy match used by the search bar across name, barcode, SKU and brand. bool matches(String query) { final q = query.trim().toLowerCase(); if (q.isEmpty) return true; return name.toLowerCase().contains(q) || barcode.toLowerCase().contains(q) || sku.toLowerCase().contains(q) || (brand?.toLowerCase().contains(q) ?? false) || category.label.toLowerCase().contains(q); } Product copyWith({ String? name, double? price, double? mrp, double? stock, ProductCategory? category, bool? isActive, }) { return Product( id: id, name: name ?? this.name, barcode: barcode, sku: sku, category: category ?? this.category, price: price ?? this.price, mrp: mrp ?? this.mrp, stock: stock ?? this.stock, emoji: emoji, imageUrl: imageUrl, unit: unit, gstRate: gstRate, hsnCode: hsnCode, brand: brand, isActive: isActive ?? this.isActive, ); } @override List get props => [id, name, barcode, sku, price, stock, isActive]; }