From 3a95cdf315c1c86f5ed21e1ef133ae272c063217 Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 24 Oct 2025 10:54:26 +0700 Subject: [PATCH 1/3] add: uploads api --- controllers/file_uploads.controller.js | 151 +++++++++++++++++++++++++ db/file_uploads.db.js | 92 +++++++++++++++ middleware/uploads.js | 69 +++++++++++ routes/file_uploads.route.js | 21 ++++ 4 files changed, 333 insertions(+) create mode 100644 controllers/file_uploads.controller.js create mode 100644 db/file_uploads.db.js create mode 100644 middleware/uploads.js create mode 100644 routes/file_uploads.route.js diff --git a/controllers/file_uploads.controller.js b/controllers/file_uploads.controller.js new file mode 100644 index 0000000..d6ce4c0 --- /dev/null +++ b/controllers/file_uploads.controller.js @@ -0,0 +1,151 @@ +const path = require("path"); +const fs = require("fs"); +const { setResponse } = require("../helpers/utils"); +const { + createFileUploadDb, + deleteFileUploadByPathDb, +} = require("../db/file_uploads.db"); +const { + createSolutionDb, +} = require("../db/brand.db"); + +const uploadFile = async (req, res) => { + try { + if (!req.file) { + const response = await setResponse([], "Tidak ada file yang diunggah", 400); + return res.status(400).json(response); + } + + const file = req.file; + const ext = path.extname(file.originalname).toLowerCase(); + const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; + const folder = typeDoc === "PDF" ? "pdf" : "images"; + + const pathDocument = `${folder}/${file.filename}`; + + // Insert ke DB via DB layer + const fileData = { + file_upload_name: file.originalname, + path_document: pathDocument, + type_document: typeDoc, + createdBy: req.user?.user_id || null, + }; + + await createFileUploadDb(fileData); + + const response = await setResponse( + { name: file.originalname, path: pathDocument }, + "File berhasil diunggah" + ); + res.status(200).json(response); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +const getFile = (folder) => async (req, res) => { + try { + const { filename } = req.params; + const filePath = path.join(__dirname, "../uploads", folder, filename); + + if (!fs.existsSync(filePath)) { + const response = await setResponse([], "File tidak ditemukan", 404); + return res.status(404).json(response); + } + + res.sendFile(filePath); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +const deleteFile = (folder) => async (req, res) => { + try { + const { filename } = req.params; + const filePath = path.join(__dirname, "../uploads", folder, filename); + + if (!fs.existsSync(filePath)) { + const response = await setResponse([], "File tidak ditemukan", 404); + return res.status(404).json(response); + } + + // Delete physical file + fs.unlinkSync(filePath); + + const pathDocument = `${folder}/${filename}`; + const deletedBy = req.user?.user_id || null; + await deleteFileUploadByPathDb(pathDocument, deletedBy); + + const response = await setResponse([], "File berhasil dihapus"); + res.status(200).json(response); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +const uploadSolutionFile = async (req, res) => { + try { + if (!req.file) { + const response = await setResponse([], "Tidak ada file yang diunggah", 400); + return res.status(400).json(response); + } + + const { error_code_id, solution_name } = req.body; + + if (!error_code_id || !solution_name) { + const response = await setResponse([], "error_code_id dan solution_name harus diisi", 400); + return res.status(400).json(response); + } + + const file = req.file; + const ext = path.extname(file.originalname).toLowerCase(); + const typeDoc = ext === ".pdf" ? "PDF" : "IMAGE"; + const folder = typeDoc === "PDF" ? "pdf" : "images"; + const pathDocument = `${folder}/${file.filename}`; + + const fileData = { + file_upload_name: file.originalname, + path_document: pathDocument, + type_document: typeDoc, + createdBy: req.user?.user_id || null, + }; + + await createFileUploadDb(fileData); + + const solutionData = { + solution_name: solution_name, + type_solution: typeDoc.toLowerCase(), + path_solution: pathDocument, + is_active: true, + created_by: req.user?.user_id || null + }; + + const solutionId = await createSolutionDb(error_code_id, solutionData); + + const response = await setResponse( + { + solution_id: solutionId, + solution_name: solution_name, + error_code_id: error_code_id, + file_name: file.originalname, + file_path: pathDocument, + file_type: typeDoc.toLowerCase() + }, + "Solution file berhasil diunggah" + ); + res.status(200).json(response); + } catch (error) { + const response = await setResponse([], error.message, 500); + res.status(500).json(response); + } +}; + +module.exports = { + uploadFile, + uploadSolutionFile, + getFile, + deleteFile, +}; \ No newline at end of file diff --git a/db/file_uploads.db.js b/db/file_uploads.db.js new file mode 100644 index 0000000..d10fbaf --- /dev/null +++ b/db/file_uploads.db.js @@ -0,0 +1,92 @@ +const pool = require("../config"); + +// Get file upload by path +const getFileUploadByPathDb = async (path) => { + const queryText = ` + SELECT + file_upload_id, + file_upload_name, + type_document, + path_document, + created_by, + updated_by, + deleted_by, + created_at, + updated_at, + deleted_at + FROM file_upload + WHERE path_document = $1 AND deleted_at IS NULL + `; + const result = await pool.query(queryText, [path]); + return result.recordset[0]; +}; + +// Create file upload +const createFileUploadDb = async (data) => { + const store = { + file_upload_name: data.file_upload_name, + }; + + // Add path_document if exists + if (data.path_document) { + store.path_document = data.path_document; + } + + // Add type_document if exists + if (data.type_document) { + store.type_document = data.type_document; + } + + if (data.createdBy) { + store.created_by = data.createdBy; + } + + console.log('Data to insert:', store); + + const queryText = ` + INSERT INTO file_upload (file_upload_name, path_document, type_document, created_by, created_at, updated_at) + VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + SELECT SCOPE_IDENTITY() as inserted_id; + `; + const values = [ + store.file_upload_name, + store.path_document, + store.type_document, + store.created_by || null + ]; + + // console.log('Manual Query:', queryText); + // console.log('Manual Values:', values); + + const result = await pool.query(queryText, values); + return result.recordset[0]; +}; + +// Soft delete file upload by path +const deleteFileUploadByPathDb = async (path, deletedBy = null) => { + const store = { + deleted_at: new Date(), + }; + + if (deletedBy) { + store.deleted_by = deletedBy; + } + + const whereData = { + path_document: path, + }; + + const { query: queryText, values } = pool.buildDynamicUpdate( + "file_upload", + store, + whereData + ); + await pool.query(`${queryText} AND deleted_at IS NULL`, values); + return true; +}; + +module.exports = { + getFileUploadByPathDb, + createFileUploadDb, + deleteFileUploadByPathDb, +}; \ No newline at end of file diff --git a/middleware/uploads.js b/middleware/uploads.js new file mode 100644 index 0000000..9e683d2 --- /dev/null +++ b/middleware/uploads.js @@ -0,0 +1,69 @@ +const multer = require("multer"); +const path = require("path"); +const { randomUUID } = require("crypto"); +const fs = require("fs"); + +// Fungsi untuk tentuin folder berdasarkan file type +function getFolderByType(mimetype) { + if (mimetype === "application/pdf") { + return "pdf"; + } else if (mimetype.startsWith("image/")) { + return "images"; + } + return "file"; +} + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const folderName = getFolderByType(file.mimetype); + const folderPath = path.join(__dirname, "../uploads", folderName); + + fs.mkdirSync(folderPath, { recursive: true }); + + cb(null, folderPath); + }, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const timestamp = Date.now(); + + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const seconds = String(date.getSeconds()).padStart(2, "0"); + + const formattedDate = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`; + + // Prefix berdasarkan tipe file + const prefix = file.mimetype === "application/pdf" ? "pdf" : "img"; + const uniqueId = randomUUID().slice(0, 8); + + cb(null, `${prefix}-${uniqueId}-${formattedDate}${ext}`); + }, +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|pdf/; + const allowedMimeTypes = /image\/(jpeg|jpg|png)|application\/pdf/; + + const extname = allowedTypes.test( + path.extname(file.originalname).toLowerCase() + ); + const mimetype = allowedMimeTypes.test(file.mimetype); + + if (extname && mimetype) { + return cb(null, true); + } else { + cb(new Error("File type not allowed. Only PDF and Images (JPEG, JPG, PNG) are accepted.")); + } + }, + limits: { + fileSize: 10 * 1024 * 1024, // 10MB max file size + }, +}); + +module.exports = upload; \ No newline at end of file diff --git a/routes/file_uploads.route.js b/routes/file_uploads.route.js new file mode 100644 index 0000000..53f4fbc --- /dev/null +++ b/routes/file_uploads.route.js @@ -0,0 +1,21 @@ +const router = require("express").Router(); +const upload = require("../middleware/uploads"); +const verifyToken = require("../middleware/verifyToken"); +const verifyAccess = require("../middleware/verifyAccess"); +const { + uploadFile, + getFile, + deleteFile, +} = require("../controllers/file_uploads.controller"); + +router.post("/", verifyToken.verifyAccessToken, verifyAccess(), upload.single("file"), uploadFile); + +router.route("/pdf/:filename") + .get(verifyToken.verifyAccessToken, getFile("pdf")) + .delete(verifyToken.verifyAccessToken, verifyAccess(), deleteFile("pdf")); + +router.route("/images/:filename") + .get(verifyToken.verifyAccessToken, getFile("images")) + .delete(verifyToken.verifyAccessToken, verifyAccess(), deleteFile("images")); + +module.exports = router; \ No newline at end of file From 893f177abdcaf49fe87dc120252225f2e238e14c Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 24 Oct 2025 11:09:30 +0700 Subject: [PATCH 2/3] add: crud brand --- controllers/brand.controller.js | 115 ++++++++++++++ db/brand.db.js | 139 ++++++++++++----- db/brand_code.db.js | 94 +++++++++++ db/brand_code_solution.db.js | 73 +++++++++ routes/brand.route.js | 18 +++ routes/index.js | 4 + services/brand.service.js | 268 ++++++++++++++++++++++++++++++++ 7 files changed, 669 insertions(+), 42 deletions(-) create mode 100644 controllers/brand.controller.js create mode 100644 db/brand_code.db.js create mode 100644 db/brand_code_solution.db.js create mode 100644 routes/brand.route.js create mode 100644 services/brand.service.js 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 From ea2905d55869fc7e01504f38b26a7860bfe08e7c Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Fri, 24 Oct 2025 11:09:50 +0700 Subject: [PATCH 3/3] add: brand validate --- validate/brand.schema.js | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 validate/brand.schema.js diff --git a/validate/brand.schema.js b/validate/brand.schema.js new file mode 100644 index 0000000..ac6cf98 --- /dev/null +++ b/validate/brand.schema.js @@ -0,0 +1,78 @@ +const Joi = require("joi"); + +// ======================== +// Brand Validation +// ======================== +const insertBrandSchema = Joi.object({ + brand_name: Joi.string().max(100).required(), + brand_type: Joi.string().max(50).optional(), + brand_manufacture: Joi.string().max(100).optional(), + brand_model: Joi.string().max(100).optional(), + is_active: Joi.boolean().required(), + description: Joi.string().max(255).optional(), + error_code: Joi.array().items( + Joi.object({ + error_code: Joi.string().max(100).required(), + error_code_name: Joi.string().max(100).required(), + error_code_description: Joi.string().optional(), + is_active: Joi.boolean().required(), + solution: Joi.array().items( + Joi.object({ + solution_name: Joi.string().max(100).required(), + type_solution: Joi.string().valid('text', 'pdf', 'image', 'video', 'link').required(), + text_solution: Joi.when('type_solution', { + is: 'text', + then: Joi.string().required(), + otherwise: Joi.string().optional().allow('') + }), + path_solution: Joi.when('type_solution', { + is: 'text', + then: Joi.string().optional().allow(''), + otherwise: Joi.string().required() + }), + is_active: Joi.boolean().required() + }) + ).min(1).required() + }) + ).min(1).required() +}); + +// Update Brand Validation +const updateBrandSchema = Joi.object({ + brand_name: Joi.string().max(100).required(), + brand_type: Joi.string().max(50).optional(), + brand_manufacture: Joi.string().max(100).optional(), + brand_model: Joi.string().max(100).optional(), + is_active: Joi.boolean().required(), + description: Joi.string().max(255).optional(), + error_code: Joi.array().items( + Joi.object({ + error_code: Joi.string().max(100).required(), + error_code_name: Joi.string().max(100).required(), + error_code_description: Joi.string().optional(), + is_active: Joi.boolean().required(), + solution: Joi.array().items( + Joi.object({ + solution_name: Joi.string().max(100).required(), + type_solution: Joi.string().valid('text', 'pdf', 'image', 'video', 'link').required(), + text_solution: Joi.when('type_solution', { + is: 'text', + then: Joi.string().required(), + otherwise: Joi.string().optional().allow('') + }), + path_solution: Joi.when('type_solution', { + is: 'text', + then: Joi.string().optional().allow(''), + otherwise: Joi.string().required() + }), + is_active: Joi.boolean().optional() + }) + ).min(1).required() + }) + ).optional() +}).min(1); + +module.exports = { + insertBrandSchema, + updateBrandSchema +}; \ No newline at end of file