diff --git a/src/database/schemas/management/productsku.schema.js b/src/database/schemas/management/productsku.schema.js new file mode 100644 index 0000000..42cfa73 --- /dev/null +++ b/src/database/schemas/management/productsku.schema.js @@ -0,0 +1,26 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; + +// Define the main product SKU schema +const productSkuSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + sku: { type: String, required: true }, + product: { type: Schema.Types.ObjectId, ref: 'product', required: true }, + name: { type: String, required: true }, + description: { type: String, required: false }, + }, + { timestamps: true } +); + +// Add virtual id getter +productSkuSchema.virtual('id').get(function () { + return this._id; +}); + +// Configure JSON serialization to include virtuals +productSkuSchema.set('toJSON', { virtuals: true }); + +// Create and export the model +export const productSkuModel = mongoose.model('productSku', productSkuSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index deae175..a990b06 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -5,6 +5,7 @@ import { filamentModel } from './management/filament.schema.js'; import { gcodeFileModel } from './production/gcodefile.schema.js'; import { partModel } from './management/part.schema.js'; import { productModel } from './management/product.schema.js'; +import { productSkuModel } from './management/productsku.schema.js'; import { vendorModel } from './management/vendor.schema.js'; import { filamentStockModel } from './inventory/filamentstock.schema.js'; import { purchaseOrderModel } from './inventory/purchaseorder.schema.js'; @@ -73,6 +74,13 @@ export const models = { referenceField: '_reference', label: 'Product', }, + SKU: { + model: productSkuModel, + idField: '_id', + type: 'productSku', + referenceField: '_reference', + label: 'Product SKU', + }, VEN: { model: vendorModel, idField: '_id', diff --git a/src/index.js b/src/index.js index 2dfad4e..8f50f05 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ import { spotlightRoutes, partRoutes, productRoutes, + productSkuRoutes, vendorRoutes, materialRoutes, partStockRoutes, @@ -133,6 +134,7 @@ app.use('/gcodefiles', gcodeFileRoutes); app.use('/filaments', filamentRoutes); app.use('/parts', partRoutes); app.use('/products', productRoutes); +app.use('/productskus', productSkuRoutes); app.use('/vendors', vendorRoutes); app.use('/materials', materialRoutes); app.use('/partstocks', partStockRoutes); diff --git a/src/routes/index.js b/src/routes/index.js index 7be4722..894947e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -11,6 +11,7 @@ import filamentRoutes from './management/filaments.js'; import spotlightRoutes from './misc/spotlight.js'; import partRoutes from './management/parts.js'; import productRoutes from './management/products.js'; +import productSkuRoutes from './management/productskus.js'; import vendorRoutes from './management/vendors.js'; import materialRoutes from './management/materials.js'; import partStockRoutes from './inventory/partstocks.js'; @@ -56,6 +57,7 @@ export { spotlightRoutes, partRoutes, productRoutes, + productSkuRoutes, vendorRoutes, materialRoutes, partStockRoutes, diff --git a/src/routes/management/productskus.js b/src/routes/management/productskus.js new file mode 100644 index 0000000..126e303 --- /dev/null +++ b/src/routes/management/productskus.js @@ -0,0 +1,59 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { getFilter, convertPropertiesString } from '../../utils.js'; + +const router = express.Router(); +import { + listProductSkusRouteHandler, + getProductSkuRouteHandler, + editProductSkuRouteHandler, + newProductSkuRouteHandler, + deleteProductSkuRouteHandler, + listProductSkusByPropertiesRouteHandler, + getProductSkuStatsRouteHandler, + getProductSkuHistoryRouteHandler, +} from '../../services/management/productskus.js'; + +router.get('/', isAuthenticated, (req, res) => { + const { page, limit, property, search, sort, order } = req.query; + const allowedFilters = ['_id', 'sku', 'product', 'product._id', 'name']; + const filter = getFilter(req.query, allowedFilters); + listProductSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order); +}); + +router.get('/properties', isAuthenticated, (req, res) => { + let properties = convertPropertiesString(req.query.properties); + const allowedFilters = ['product', 'product._id']; + const filter = getFilter(req.query, allowedFilters, false); + let masterFilter = {}; + if (req.query.masterFilter) { + masterFilter = JSON.parse(req.query.masterFilter); + } + listProductSkusByPropertiesRouteHandler(req, res, properties, filter, masterFilter); +}); + +router.post('/', isAuthenticated, (req, res) => { + newProductSkuRouteHandler(req, res); +}); + +router.get('/stats', isAuthenticated, (req, res) => { + getProductSkuStatsRouteHandler(req, res); +}); + +router.get('/history', isAuthenticated, (req, res) => { + getProductSkuHistoryRouteHandler(req, res); +}); + +router.get('/:id', isAuthenticated, (req, res) => { + getProductSkuRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editProductSkuRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteProductSkuRouteHandler(req, res); +}); + +export default router; diff --git a/src/services/management/productskus.js b/src/services/management/productskus.js new file mode 100644 index 0000000..5147901 --- /dev/null +++ b/src/services/management/productskus.js @@ -0,0 +1,182 @@ +import config from '../../config.js'; +import { productSkuModel } from '../../database/schemas/management/productsku.schema.js'; +import log4js from 'log4js'; +import mongoose from 'mongoose'; +import { + deleteObject, + listObjects, + getObject, + editObject, + newObject, + listObjectsByProperties, + getModelStats, + getModelHistory, +} from '../../database/database.js'; +const logger = log4js.getLogger('Product SKUs'); +logger.level = config.server.logLevel; + +export const listProductSkusRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend' +) => { + const result = await listObjects({ + model: productSkuModel, + page, + limit, + property, + filter, + search, + sort, + order, + populate: ['product'], + }); + + if (result?.error) { + logger.error('Error listing product SKUs.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of product SKUs (Page ${page}, Limit ${limit}). Count: ${result.length}.`); + res.send(result); +}; + +export const listProductSkusByPropertiesRouteHandler = async ( + req, + res, + properties = '', + filter = {}, + masterFilter = {} +) => { + const result = await listObjectsByProperties({ + model: productSkuModel, + properties, + filter, + populate: ['product'], + masterFilter, + }); + + if (result?.error) { + logger.error('Error listing product SKUs.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of product SKUs. Count: ${result.length}`); + res.send(result); +}; + +export const getProductSkuRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: productSkuModel, + id, + populate: ['product'], + }); + if (result?.error) { + logger.warn(`Product SKU not found with supplied id.`); + return res.status(result.code).send(result); + } + logger.debug(`Retrieved product SKU with ID: ${id}`); + res.send(result); +}; + +export const editProductSkuRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Product SKU with ID: ${id}`); + + const updateData = { + updatedAt: new Date(), + sku: req.body?.sku, + product: req.body?.product, + name: req.body?.name, + description: req.body?.description, + }; + + const result = await editObject({ + model: productSkuModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing product SKU:', result.error); + res.status(result.code || 500).send(result); + return; + } + + logger.debug(`Edited product SKU with ID: ${id}`); + res.send(result); +}; + +export const newProductSkuRouteHandler = async (req, res) => { + const newData = { + sku: req.body?.sku, + product: req.body?.product, + name: req.body?.name, + description: req.body?.description, + }; + + const result = await newObject({ + model: productSkuModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No product SKU created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New product SKU with ID: ${result._id}`); + res.send(result); +}; + +export const deleteProductSkuRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Product SKU with ID: ${id}`); + + const result = await deleteObject({ + model: productSkuModel, + id, + user: req.user, + }); + if (result.error) { + logger.error('No product SKU deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Deleted product SKU with ID: ${id}`); + res.send(result); +}; + +export const getProductSkuStatsRouteHandler = async (req, res) => { + const result = await getModelStats({ model: productSkuModel }); + if (result?.error) { + logger.error('Error fetching product SKU stats:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Product SKU stats:', result); + res.send(result); +}; + +export const getProductSkuHistoryRouteHandler = async (req, res) => { + const from = req.query.from; + const to = req.query.to; + const result = await getModelHistory({ model: productSkuModel, from, to }); + if (result?.error) { + logger.error('Error fetching product SKU history:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Product SKU history:', result); + res.send(result); +}; diff --git a/src/services/misc/csv.js b/src/services/misc/csv.js index 1bcee02..140cd74 100644 --- a/src/services/misc/csv.js +++ b/src/services/misc/csv.js @@ -65,6 +65,7 @@ function getModelFilterFields(objectType) { filamentStock: ['filament'], partStock: ['part'], productStock: ['product'], + productSku: ['product'], purchaseOrder: ['vendor'], orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], shipment: ['order._id', 'orderType', 'courierService._id'], diff --git a/src/services/misc/excel.js b/src/services/misc/excel.js index d8afa65..54795e4 100644 --- a/src/services/misc/excel.js +++ b/src/services/misc/excel.js @@ -71,6 +71,7 @@ function getModelFilterFields(objectType) { filamentStock: ['filament'], partStock: ['part'], productStock: ['product'], + productSku: ['product'], purchaseOrder: ['vendor'], orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], shipment: ['order._id', 'orderType', 'courierService._id'], diff --git a/src/services/misc/odata.js b/src/services/misc/odata.js index 901f627..9f96a64 100644 --- a/src/services/misc/odata.js +++ b/src/services/misc/odata.js @@ -345,6 +345,7 @@ function getModelFilterFields(objectType) { filamentStock: ['filament'], partStock: ['part'], productStock: ['product'], + productSku: ['product'], purchaseOrder: ['vendor'], orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], shipment: ['order._id', 'orderType', 'courierService._id'],