diff --git a/controllers/brand.controller.js b/controllers/brand.controller.js new file mode 100644 index 0000000..9e1f73e --- /dev/null +++ b/controllers/brand.controller.js @@ -0,0 +1,115 @@ +const BrandService = require('../services/brand.service'); +const { setResponse, setResponsePaging, checkValidate } = require('../helpers/utils'); +const { createFileUploadDb } = require('../db/file_uploads.db'); +const { + insertBrandSchema, + updateBrandSchema, + uploadSolutionSchema +} = require('../validate/brand.schema'); + +class BrandController { + // Get all brands + static async getAll(req, res) { + const queryParams = req.query; + + const results = await BrandService.getAllBrands(queryParams); + const response = await setResponsePaging(queryParams, results, 'Brand found'); + + res.status(response.statusCode).json(response); + } + + // Get brand by ID + static async getById(req, res) { + const { id } = req.params; + + const results = await BrandService.getBrandById(id); + const response = await setResponse(results, 'Brand found'); + + res.status(response.statusCode).json(response); + } + + // Create brand with nested error codes and solutions + static async create(req, res) { + const { error, value } = await checkValidate(insertBrandSchema, req); + + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + value.created_by = req.user?.user_id || null; + + const results = await BrandService.createBrandWithFullData(value); + const response = await setResponse(results, 'Brand created successfully'); + + return res.status(response.statusCode).json(response); + } + + // Update brand + static async update(req, res) { + const { id } = req.params; + + const { error, value } = await checkValidate(updateBrandSchema, req); + if (error) { + return res.status(400).json(setResponse(error, 'Validation failed', 400)); + } + + try { + if (req.file) { + const file = req.file; + const ext = require('path').extname(file.originalname).toLowerCase(); + const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; + const folder = typeDoc === "PDF" ? "pdf" : "images"; + const pathDocument = `${folder}/${file.filename}`; + + // Insert to file_upload table + const fileData = { + file_upload_name: file.originalname, + createdBy: req.user?.user_id || null, + }; + await createFileUploadDb(fileData); + + if (value.error_code && Array.isArray(value.error_code)) { + for (const errorCode of value.error_code) { + if (errorCode.solution && Array.isArray(errorCode.solution)) { + for (const solution of errorCode.solution) { + if (solution.type_solution !== 'text' && (!solution.path_solution || solution.path_solution === '')) { + solution.path_solution = pathDocument; + solution.type_solution = typeDoc.toLowerCase(); + } + } + } + } + } + } + + value.updated_by = req.user?.user_id || null; + + const results = await BrandService.updateBrandWithFullData(id, value); + const response = await setResponse(results, 'Brand updated successfully'); + + res.status(response.statusCode).json(response); + + } catch (error) { + const response = setResponse([], error.message, error.statusCode || 500); + res.status(response.statusCode).json(response); + } + } + + // Soft delete brand by ID + static async delete(req, res) { + const { id } = req.params; + // Get brand by ID first to get name for deletion + const brand = await BrandService.getBrandById(id); + if (!brand) { + const response = await setResponse([], 'Brand not found', 404); + return res.status(response.statusCode).json(response); + } + + const results = await BrandService.deleteBrand(brand.brand_name, req.user.user_id); + const response = await setResponse(results, 'Brand deleted successfully'); + + res.status(response.statusCode).json(response); + } +} + +module.exports = BrandController; \ No newline at end of file diff --git a/db/brand.db.js b/db/brand.db.js index 3385e8f..0d985b4 100644 --- a/db/brand.db.js +++ b/db/brand.db.js @@ -4,23 +4,28 @@ const pool = require("../config"); const getAllBrandsDb = async (searchParams = {}) => { let queryParams = []; + // Pagination if (searchParams.limit) { const page = Number(searchParams.page ?? 1) - 1; queryParams = [Number(searchParams.limit ?? 10), page]; } + // Search const { whereOrConditions, whereParamOr } = pool.buildStringOrIlike( - ["b.brand_name"], + ["a.brand_name", "a.brand_type", "a.brand_manufacture", "a.brand_model", "a.brand_code"], searchParams.criteria, queryParams ); queryParams = whereParamOr ? whereParamOr : queryParams; + // Filter const { whereConditions, whereParamAnd } = pool.buildFilterQuery( [ - { column: "b.brand_name", param: searchParams.name, type: "string" }, - { column: "b.created_by", param: searchParams.created_by, type: "number" }, + { column: "a.brand_type", param: searchParams.type, type: "string" }, + { column: "a.brand_manufacture", param: searchParams.manufacture, type: "string" }, + { column: "a.brand_model", param: searchParams.model, type: "string" }, + { column: "a.is_active", param: searchParams.status, type: "string" }, ], queryParams ); @@ -28,49 +33,57 @@ const getAllBrandsDb = async (searchParams = {}) => { queryParams = whereParamAnd ? whereParamAnd : queryParams; const queryText = ` - SELECT COUNT(*) OVER() AS total_data, b.* - FROM m_brands b - WHERE b.deleted_at IS NULL - ${whereConditions.length > 0 ? ` AND ${whereConditions.join(" AND ")}` : ""} - ${whereOrConditions ? whereOrConditions : ""} - ORDER BY b.brand_id ASC + SELECT + COUNT(*) OVER() AS total_data, + a.* + FROM m_brands a + WHERE a.deleted_at IS NULL + ${whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : ''} + ${whereOrConditions ? whereOrConditions : ''} + ORDER BY a.brand_id ASC ${searchParams.limit ? `OFFSET $2 * $1 ROWS FETCH NEXT $1 ROWS ONLY` : ''} `; const result = await pool.query(queryText, queryParams); - - const total = result?.recordset.length > 0 ? parseInt(result.recordset[0].total_data, 10) : 0; + const total = + result?.recordset.length > 0 + ? parseInt(result.recordset[0].total_data, 10) + : 0; return { data: result.recordset, total }; }; -// Get brand by ID -const getBrandByIdDb = async (id) => { +// Get brand by name (path-based) +const getBrandByNameDb = async (brandName) => { const queryText = ` - SELECT b.* - FROM m_brands b - WHERE b.brand_id = $1 AND b.deleted_at IS NULL + SELECT + a.* + FROM m_brands a + WHERE a.brand_name = $1 AND a.deleted_at IS NULL `; - const result = await pool.query(queryText, [id]); - return result.recordset; + const result = await pool.query(queryText, [brandName]); + return result.recordset[0]; }; -// Get brand by name -const getBrandByNameDb = async (name) => { +// Get brand by ID (for internal use) +const getBrandByIdDb = async (id) => { const queryText = ` - SELECT b.* - FROM m_brands b - WHERE b.brand_name = $1 AND b.deleted_at IS NULL + SELECT + a.* + FROM m_brands a + WHERE a.brand_id = $1 AND a.deleted_at IS NULL `; - const result = await pool.query(queryText, [name]); + const result = await pool.query(queryText, [id]); return result.recordset[0]; }; // Create brand const createBrandDb = async (data) => { + const newCode = await pool.generateKode("BRD", "m_brands", "brand_code"); + const store = { ...data, - created_at: new Date(), + brand_code: newCode, }; const { query: queryText, values } = pool.buildDynamicInsert("m_brands", store); @@ -79,38 +92,80 @@ const createBrandDb = async (data) => { return insertedId ? await getBrandByIdDb(insertedId) : null; }; -// Update brand -const updateBrandDb = async (id, data) => { - const store = { - ...data, - updated_at: new Date(), - }; - - const whereData = { - brand_id: id, - }; +// Update brand by name +const updateBrandDb = async (brandName, data) => { + const store = { ...data }; + const whereData = { brand_name: brandName }; const { query: queryText, values } = pool.buildDynamicUpdate("m_brands", store, whereData); await pool.query(`${queryText} AND deleted_at IS NULL`, values); - return getBrandByIdDb(id); + return getBrandByNameDb(brandName); }; -// Soft delete brand -const deleteBrandDb = async (id, deletedBy) => { +// Soft delete brand by name +const deleteBrandDb = async (brandName, deletedBy) => { const queryText = ` UPDATE m_brands SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 - WHERE brand_id = $2 AND deleted_at IS NULL + WHERE brand_name = $2 AND deleted_at IS NULL `; - await pool.query(queryText, [deletedBy, id]); + await pool.query(queryText, [deletedBy, brandName]); return true; }; +// Check if brand name exists (for validation) +const checkBrandNameExistsDb = async (brandName, excludeId = null) => { + let queryText = ` + SELECT brand_id + FROM m_brands + WHERE brand_name = $1 AND deleted_at IS NULL + `; + let values = [brandName]; + + if (excludeId) { + queryText += ` AND brand_id != $2`; + values.push(excludeId); + } + + const result = await pool.query(queryText, values); + return result.recordset.length > 0; +}; + +// Get brand with error codes count +const getBrandsWithErrorCodeCountDb = async (searchParams = {}) => { + let queryParams = []; + + const queryText = ` + SELECT + a.brand_id, + a.brand_name, + a.brand_type, + a.brand_manufacture, + a.brand_model, + a.brand_code, + a.is_active, + a.created_at, + COUNT(bc.error_code_id) as error_code_count + FROM m_brands a + LEFT JOIN brand_code bc ON a.brand_id = bc.brand_id AND bc.deleted_at IS NULL + WHERE a.deleted_at IS NULL + GROUP BY + a.brand_id, a.brand_name, a.brand_type, a.brand_manufacture, + a.brand_model, a.brand_code, a.is_active, a.created_at + ORDER BY a.brand_name + `; + + const result = await pool.query(queryText, queryParams); + return result.recordset; +}; + module.exports = { getAllBrandsDb, - getBrandByIdDb, getBrandByNameDb, + getBrandByIdDb, createBrandDb, updateBrandDb, deleteBrandDb, -}; + checkBrandNameExistsDb, + getBrandsWithErrorCodeCountDb, +}; \ No newline at end of file diff --git a/db/brand_code.db.js b/db/brand_code.db.js new file mode 100644 index 0000000..aac38b7 --- /dev/null +++ b/db/brand_code.db.js @@ -0,0 +1,94 @@ +const pool = require("../config"); + +// Get error codes by brand ID +const getErrorCodesByBrandIdDb = async (brandId) => { + const queryText = ` + SELECT + a.* + FROM brand_code a + WHERE a.brand_id = $1 AND a.deleted_at IS NULL + ORDER BY a.error_code_id + `; + const result = await pool.query(queryText, [brandId]); + return result.recordset; +}; + +// Get error code by brand ID and error code +const getErrorCodeByBrandIdAndCodeDb = async (brandId, errorCode) => { + const queryText = ` + SELECT + a.* + FROM brand_code a + WHERE a.brand_id = $1 AND a.error_code = $2 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [brandId, errorCode]); + return result.recordset[0]; +}; + +// Create error code for brand +const createErrorCodeDb = async (brandId, data) => { + const store = { + brand_id: brandId, + error_code: data.error_code, + error_code_name: data.error_code_name, + error_code_description: data.error_code_description, + is_active: data.is_active, + created_by: data.created_by + }; + + const { query: queryText, values } = pool.buildDynamicInsert("brand_code", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId; +}; + +// Update error code by brand ID and error code +const updateErrorCodeDb = async (brandId, errorCode, data) => { + const store = { ...data }; + const whereData = { + brand_id: brandId, + error_code: errorCode + }; + + const { query: queryText, values } = pool.buildDynamicUpdate("brand_code", store, whereData); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return true; +}; + +// Soft delete error code by brand ID and error code +const deleteErrorCodeDb = async (brandId, errorCode, deletedBy) => { + const queryText = ` + UPDATE brand_code + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE brand_id = $2 AND error_code = $3 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, brandId, errorCode]); + return true; +}; + +// Check if error code exists for brand +const checkErrorCodeExistsDb = async (brandId, errorCode, excludeId = null) => { + let queryText = ` + SELECT error_code_id + FROM brand_code + WHERE brand_id = $1 AND error_code = $2 AND deleted_at IS NULL + `; + let values = [brandId, errorCode]; + + if (excludeId) { + queryText += ` AND error_code_id != $3`; + values.push(excludeId); + } + + const result = await pool.query(queryText, values); + return result.recordset.length > 0; +}; + +module.exports = { + getErrorCodesByBrandIdDb, + getErrorCodeByBrandIdAndCodeDb, + createErrorCodeDb, + updateErrorCodeDb, + deleteErrorCodeDb, + checkErrorCodeExistsDb, +}; \ No newline at end of file diff --git a/db/brand_code_solution.db.js b/db/brand_code_solution.db.js new file mode 100644 index 0000000..92283bd --- /dev/null +++ b/db/brand_code_solution.db.js @@ -0,0 +1,73 @@ +const pool = require("../config"); + +// Get solutions by error code ID +const getSolutionsByErrorCodeIdDb = async (errorCodeId) => { + const queryText = ` + SELECT + a.* + FROM brand_code_solution a + WHERE a.error_code_id = $1 AND a.deleted_at IS NULL + ORDER BY a.brand_code_solution_id + `; + const result = await pool.query(queryText, [errorCodeId]); + return result.recordset; +}; + +// Create solution for error code +const createSolutionDb = async (errorCodeId, data) => { + const store = { + error_code_id: errorCodeId, + solution_name: data.solution_name, + type_solution: data.type_solution, + text_solution: data.text_solution, + path_solution: data.path_solution, + is_active: data.is_active, + created_by: data.created_by + }; + + const { query: queryText, values } = pool.buildDynamicInsert("brand_code_solution", store); + const result = await pool.query(queryText, values); + const insertedId = result.recordset[0]?.inserted_id; + return insertedId; +}; + +// Update solution +const updateSolutionDb = async (solutionId, data) => { + const store = { ...data }; + const whereData = { brand_code_solution_id: solutionId }; + + const { query: queryText, values } = pool.buildDynamicUpdate("brand_code_solution", store, whereData); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return true; +}; + +// Soft delete solution +const deleteSolutionDb = async (solutionId, deletedBy) => { + const queryText = ` + UPDATE brand_code_solution + SET deleted_at = CURRENT_TIMESTAMP, deleted_by = $1 + WHERE brand_code_solution_id = $2 AND deleted_at IS NULL + `; + await pool.query(queryText, [deletedBy, solutionId]); + return true; +}; + +// Get solution by ID +const getSolutionByIdDb = async (solutionId) => { + const queryText = ` + SELECT + a.* + FROM brand_code_solution a + WHERE a.brand_code_solution_id = $1 AND a.deleted_at IS NULL + `; + const result = await pool.query(queryText, [solutionId]); + return result.recordset[0]; +}; + +module.exports = { + getSolutionsByErrorCodeIdDb, + createSolutionDb, + updateSolutionDb, + deleteSolutionDb, + getSolutionByIdDb, +}; \ No newline at end of file diff --git a/routes/brand.route.js b/routes/brand.route.js new file mode 100644 index 0000000..fa71fa0 --- /dev/null +++ b/routes/brand.route.js @@ -0,0 +1,18 @@ +const express = require('express'); +const BrandController = require('../controllers/brand.controller'); +const verifyToken = require('../middleware/verifyToken'); +const verifyAccess = require('../middleware/verifyAccess'); +const upload = require('../middleware/uploads'); + +const router = express.Router(); + +router.route('/') + .get(verifyToken.verifyAccessToken, BrandController.getAll) + .post(verifyToken.verifyAccessToken, verifyAccess(), BrandController.create); + +router.route('/:id') + .get(verifyToken.verifyAccessToken, BrandController.getById) + .put(verifyToken.verifyAccessToken, verifyAccess(), upload.single('file'), BrandController.update) + .delete(verifyToken.verifyAccessToken, verifyAccess(), BrandController.delete); + +module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index a5360a3..8bbd80e 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,7 +4,9 @@ const users = require("./users.route"); const device = require('./device.route'); const roles = require('./roles.route'); const tags = require("./tags.route"); +const brand = require("./brand.route"); const subSection = require("./plant_sub_section.route"); +const fileUploads = require("./file_uploads.route"); const shift = require("./shift.route"); const schedule = require("./schedule.route"); const status = require("./status.route"); @@ -16,7 +18,9 @@ router.use("/user", users); router.use("/device", device); router.use("/roles", roles); router.use("/tags", tags); +router.use("/brand", brand); router.use("/plant-sub-section", subSection); +router.use("/file-uploads", fileUploads); router.use("/shift", shift); router.use("/schedule", schedule); router.use("/status", status); diff --git a/services/brand.service.js b/services/brand.service.js new file mode 100644 index 0000000..9bb2884 --- /dev/null +++ b/services/brand.service.js @@ -0,0 +1,268 @@ +// Brand operations +const { + getAllBrandsDb, + getBrandByIdDb, + getBrandByNameDb, + createBrandDb, + updateBrandDb, + deleteBrandDb, + checkBrandNameExistsDb, +} = require('../db/brand.db'); + +// Error code operations +const { + getErrorCodesByBrandIdDb, + createErrorCodeDb, + updateErrorCodeDb, + deleteErrorCodeDb, + checkErrorCodeExistsDb, +} = require('../db/brand_code.db'); + +// Solution operations +const { + getSolutionsByErrorCodeIdDb, + createSolutionDb, + updateSolutionDb, + deleteSolutionDb, +} = require('../db/brand_code_solution.db'); +const { getFileUploadByPathDb } = require('../db/file_uploads.db'); +const { ErrorHandler } = require('../helpers/error'); + +class BrandService { + // Get all brands + static async getAllBrands(param) { + try { + const results = await getAllBrandsDb(param); + + results.data.map(element => { + }); + + return results; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Get brand by ID with complete data + static async getBrandById(id) { + try { + const brand = await getBrandByIdDb(id); + if (!brand) throw new ErrorHandler(404, 'Brand not found'); + + const errorCodes = await getErrorCodesByBrandIdDb(brand.brand_id); + + const errorCodesWithSolutions = await Promise.all( + errorCodes.map(async (errorCode) => { + const solutions = await getSolutionsByErrorCodeIdDb(errorCode.error_code_id); + + const solutionsWithFiles = await Promise.all( + solutions.map(async (solution) => { + let fileData = null; + if (solution.path_solution && solution.type_solution !== 'text') { + fileData = await getFileUploadByPathDb(solution.path_solution); + } + + return { + ...solution, + file_upload_name: fileData?.file_upload_name || null, + path_document: fileData?.path_document || null + }; + }) + ); + + return { + ...errorCode, + solution: solutionsWithFiles + }; + }) + ); + + return { + ...brand, + error_code: errorCodesWithSolutions + }; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Create brand + static async createBrandWithFullData(data) { + try { + if (!data || typeof data !== 'object') data = {}; + + if (data.brand_name) { + const brandExists = await checkBrandNameExistsDb(data.brand_name); + if (brandExists) { + throw new ErrorHandler(400, 'Brand name already exists'); + } + } + + if (!data.error_code || !Array.isArray(data.error_code) || data.error_code.length === 0) { + throw new ErrorHandler(400, 'Brand must have at least 1 error code with solution'); + } + + for (const errorCode of data.error_code) { + if (!errorCode.solution || !Array.isArray(errorCode.solution) || errorCode.solution.length === 0) { + throw new ErrorHandler(400, `Error code ${errorCode.error_code} must have at least 1 solution`); + } + } + + const brandData = { + brand_name: data.brand_name, + brand_type: data.brand_type, + brand_manufacture: data.brand_manufacture, + brand_model: data.brand_model, + is_active: data.is_active, + created_by: data.created_by + }; + + const createdBrand = await createBrandDb(brandData); + if (!createdBrand) { + throw new Error('Failed to create brand'); + } + + const brandId = createdBrand.brand_id; + + for (const errorCodeData of data.error_code) { + // Use separate db function for error codes + const errorId = await createErrorCodeDb(brandId, { + error_code: errorCodeData.error_code, + error_code_name: errorCodeData.error_code_name, + error_code_description: errorCodeData.error_code_description, + is_active: errorCodeData.is_active, + created_by: data.created_by + }); + + if (!errorId) { + throw new Error('Failed to create error code'); + } + + // Create solutions for this error code + if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) { + for (const solutionData of errorCodeData.solution) { + // Use separate db function for solutions + await createSolutionDb(errorId, { + solution_name: solutionData.solution_name, + type_solution: solutionData.type_solution, + text_solution: solutionData.text_solution || null, + path_solution: solutionData.path_solution || null, + is_active: solutionData.is_active, + created_by: data.created_by + }); + } + } + } + + return await this.getBrandById(brandId); + + } catch (error) { + throw new ErrorHandler(500, `Bulk insert failed: ${error.message}`); + } + } + + // Soft delete brand by name (convert to ID for database operation) + static async deleteBrand(brandName, userId) { + try { + // Get brand by name first to get ID + const brandExist = await getBrandByNameDb(brandName); + + if (!brandExist) { + throw new ErrorHandler(404, 'Brand not found'); + } + + const result = await deleteBrandDb(brandName, userId); + + return result; + } catch (error) { + throw new ErrorHandler(error.statusCode, error.message); + } + } + + // Update brand + static async updateBrandWithFullData(id, data) { + try { + const existingBrand = await getBrandByIdDb(id); + if (!existingBrand) throw new ErrorHandler(404, 'Brand not found'); + + // Check if brand name already exists (excluding current brand) + if (data.brand_name && data.brand_name !== existingBrand.brand_name) { + const brandExists = await checkBrandNameExistsDb(data.brand_name, id); + if (brandExists) { + throw new ErrorHandler(400, 'Brand name already exists'); + } + } + + const brandData = { + brand_name: data.brand_name, + brand_type: data.brand_type, + brand_manufacture: data.brand_manufacture, + brand_model: data.brand_model, + is_active: data.is_active, + updated_by: data.updated_by + }; + + await updateBrandDb(existingBrand.brand_name, brandData); + + if (data.error_code && Array.isArray(data.error_code)) { + const existingErrorCodes = await getErrorCodesByBrandIdDb(id); + + // Create/update/delete error codes + for (const errorCodeData of data.error_code) { + // Check if error code already exists + const existingEC = existingErrorCodes.find(ec => ec.error_code === errorCodeData.error_code); + + if (existingEC) { + // Update existing error code using separate db function + await updateErrorCodeDb(existingEC.brand_id, existingEC.error_code, { + error_code_name: errorCodeData.error_code_name, + error_code_description: errorCodeData.error_code_description, + is_active: errorCodeData.is_active, + updated_by: data.updated_by + }); + + if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) { + for (const solutionData of errorCodeData.solution) { + await createSolutionDb(existingEC.error_code_id, { + solution_name: solutionData.solution_name, + type_solution: solutionData.type_solution, + text_solution: solutionData.text_solution || null, + path_solution: solutionData.path_solution || null, + is_active: solutionData.is_active, + created_by: data.updated_by + }); + } + } + } else { + const errorId = await createErrorCodeDb(id, { + error_code: errorCodeData.error_code, + error_code_name: errorCodeData.error_code_name, + error_code_description: errorCodeData.error_code_description, + is_active: errorCodeData.is_active, + created_by: data.updated_by + }); + + if (errorCodeData.solution && Array.isArray(errorCodeData.solution)) { + for (const solutionData of errorCodeData.solution) { + await createSolutionDb(errorId, { + solution_name: solutionData.solution_name, + type_solution: solutionData.type_solution, + text_solution: solutionData.text_solution || null, + path_solution: solutionData.path_solution || null, + is_active: solutionData.is_active, + created_by: data.updated_by + }); + } + } + } + } + } + + return await this.getBrandById(id); + } catch (error) { + throw new ErrorHandler(500, `Update failed: ${error.message}`); + } + } +} + +module.exports = BrandService; \ No newline at end of file