Files

235 lines
6.4 KiB
JavaScript

const database = require('./database');
class Product {
constructor(data = {}) {
this.id = data.id || null;
this.name = data.name || '';
this.description = data.description || '';
this.category = data.category || '';
this.quantity = data.quantity || 0;
this.unit = data.unit || '';
this.barcode = data.barcode || null;
this.qr_code = data.qr_code || null;
this.location = data.location || '';
this.min_stock_level = data.min_stock_level || 0;
this.created_at = data.created_at || null;
this.updated_at = data.updated_at || null;
}
/**
* Validate product data
* @returns {Object} validation result with isValid boolean and errors array
*/
validate() {
const errors = [];
// Required field validation
if (!this.name || this.name.trim().length === 0) {
errors.push('Product name is required');
}
if (this.name && this.name.length > 255) {
errors.push('Product name must be less than 255 characters');
}
// Quantity validation
if (typeof this.quantity !== 'number' || this.quantity < 0) {
errors.push('Quantity must be a non-negative number');
}
// Min stock level validation
if (typeof this.min_stock_level !== 'number' || this.min_stock_level < 0) {
errors.push('Minimum stock level must be a non-negative number');
}
// Barcode validation (if provided)
if (this.barcode && (typeof this.barcode !== 'string' || this.barcode.trim().length === 0)) {
errors.push('Barcode must be a non-empty string if provided');
}
// Category validation
if (this.category && this.category.length > 100) {
errors.push('Category must be less than 100 characters');
}
// Unit validation
if (this.unit && this.unit.length > 20) {
errors.push('Unit must be less than 20 characters');
}
return {
isValid: errors.length === 0,
errors: errors
};
}
/**
* Save product to database
* @returns {Promise<Product>} saved product instance
*/
async save() {
const validation = this.validate();
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
const db = database.getDatabase();
try {
if (this.id) {
// Update existing product
const stmt = db.prepare(`
UPDATE items
SET name = ?, description = ?, category = ?, quantity = ?,
unit = ?, barcode = ?, qr_code = ?, location = ?,
min_stock_level = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(
this.name, this.description, this.category, this.quantity,
this.unit, this.barcode, this.qr_code, this.location,
this.min_stock_level, this.id
);
} else {
// Insert new product
const stmt = db.prepare(`
INSERT INTO items (name, description, category, quantity, unit, barcode, qr_code, location, min_stock_level)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
this.name, this.description, this.category, this.quantity,
this.unit, this.barcode, this.qr_code, this.location,
this.min_stock_level
);
this.id = result.lastInsertRowid;
}
return this;
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new Error('A product with this barcode already exists');
}
throw error;
}
}
/**
* Find product by ID
* @param {number} id - Product ID
* @returns {Promise<Product|null>} Product instance or null if not found
*/
static async findById(id) {
const db = database.getDatabase();
const stmt = db.prepare('SELECT * FROM items WHERE id = ?');
const row = stmt.get(id);
return row ? new Product(row) : null;
}
/**
* Find product by barcode
* @param {string} barcode - Product barcode
* @returns {Promise<Product|null>} Product instance or null if not found
*/
static async findByBarcode(barcode) {
const db = database.getDatabase();
const stmt = db.prepare('SELECT * FROM items WHERE barcode = ?');
const row = stmt.get(barcode);
return row ? new Product(row) : null;
}
/**
* Get all products with optional filtering
* @param {Object} filters - Optional filters
* @returns {Promise<Product[]>} Array of Product instances
*/
static async findAll(filters = {}) {
const db = database.getDatabase();
let query = 'SELECT * FROM items';
const params = [];
const conditions = [];
if (filters.category) {
conditions.push('category = ?');
params.push(filters.category);
}
if (filters.name) {
conditions.push('name LIKE ?');
params.push(`%${filters.name}%`);
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY name';
const stmt = db.prepare(query);
const rows = stmt.all(...params);
return rows.map(row => new Product(row));
}
/**
* Delete product by ID
* @param {number} id - Product ID
* @returns {Promise<boolean>} true if deleted, false if not found
*/
static async deleteById(id) {
const db = database.getDatabase();
const stmt = db.prepare('DELETE FROM items WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
/**
* Convert product to plain object
* @returns {Object} Plain object representation
*/
toJSON() {
return {
id: this.id,
name: this.name,
description: this.description,
category: this.category,
quantity: this.quantity,
unit: this.unit,
barcode: this.barcode,
qr_code: this.qr_code,
location: this.location,
min_stock_level: this.min_stock_level,
created_at: this.created_at,
updated_at: this.updated_at
};
}
/**
* Get all unique categories
* @returns {Promise<Array>} Array of unique category names
*/
static async getCategories() {
const db = database.getDatabase();
try {
const stmt = db.prepare(`
SELECT DISTINCT category
FROM products
WHERE category IS NOT NULL AND category != ''
ORDER BY category
`);
const rows = stmt.all();
return rows.map(row => row.category);
} catch (error) {
console.error('Error getting categories:', error);
throw error;
}
}
}
module.exports = Product;