From cd198e92720706224aa8e28efe65757ac7147440 Mon Sep 17 00:00:00 2001 From: Rynare Date: Thu, 15 May 2025 09:07:07 +0700 Subject: [PATCH] menambahkan crud suppliers --- package-lock.json | 97 ++++++++++++- package.json | 3 + src/controller/api/productsController.ts | 129 ----------------- src/controller/api/stockForecasting.ts | 134 ++++++++++-------- src/controller/api/suppliers.ts | 101 ------------- src/controller/api/suppliersController.ts | 121 ++++++++++++++++ src/middleware/fileUploader.ts | 62 ++++++++ src/repository/suppliersRepository.ts | 44 ++++++ src/routes/api/index.ts | 53 +------ src/routes/api/prediction.ts | 0 src/routes/api/supplier.ts | 13 ++ src/services/supplierServices.ts | 31 ++++ src/temp/dummy_time_series-1747066831499.xlsx | Bin 0 -> 10086 bytes src/types/core/global/env.d.ts | 2 + src/types/core/global/http.d.ts | 1 - src/types/core/http.ts | 16 ++- 16 files changed, 456 insertions(+), 351 deletions(-) delete mode 100644 src/controller/api/suppliers.ts create mode 100644 src/controller/api/suppliersController.ts create mode 100644 src/middleware/fileUploader.ts create mode 100644 src/repository/suppliersRepository.ts create mode 100644 src/routes/api/prediction.ts create mode 100644 src/routes/api/supplier.ts create mode 100644 src/services/supplierServices.ts create mode 100644 src/temp/dummy_time_series-1747066831499.xlsx diff --git a/package-lock.json b/package-lock.json index d6b2f0e..0b5f764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,10 +23,12 @@ "http-errors": "~1.6.3", "jsonwebtoken": "^9.0.0", "knex": "^2.5.1", + "libphonenumber-js": "^1.12.8", "moment": "^2.29.4", "mongodb": "^6.14.0", "mongoose": "^8.11.0", "morgan": "~1.9.1", + "multer": "^1.4.5-lts.2", "mysql": "^2.18.1", "mysql2": "^3.14.1", "node-schedule": "^2.1.1", @@ -42,6 +44,7 @@ "@types/express": "^5.0.1", "@types/jsonwebtoken": "^9.0.9", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.12", "@types/nodemailer": "^6.4.17", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", @@ -311,6 +314,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.15.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", @@ -537,6 +550,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -756,7 +775,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/buffer-more-ints": { @@ -765,6 +783,17 @@ "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -962,6 +991,21 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -2127,6 +2171,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, + "node_modules/libphonenumber-js": { + "version": "1.12.8", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.8.tgz", + "integrity": "sha512-f1KakiQJa9tdc7w1phC2ST+DyxWimy9c3g3yeF+84QtEanJr2K77wAmBPP22riU05xldniHsvXuflnLZ4oysqA==", + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2351,7 +2401,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2629,6 +2678,36 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mysql": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", @@ -3586,6 +3665,14 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3871,6 +3958,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", diff --git a/package.json b/package.json index 666622a..c48674e 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,12 @@ "http-errors": "~1.6.3", "jsonwebtoken": "^9.0.0", "knex": "^2.5.1", + "libphonenumber-js": "^1.12.8", "moment": "^2.29.4", "mongodb": "^6.14.0", "mongoose": "^8.11.0", "morgan": "~1.9.1", + "multer": "^1.4.5-lts.2", "mysql": "^2.18.1", "mysql2": "^3.14.1", "node-schedule": "^2.1.1", @@ -43,6 +45,7 @@ "@types/express": "^5.0.1", "@types/jsonwebtoken": "^9.0.9", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.12", "@types/nodemailer": "^6.4.17", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", diff --git a/src/controller/api/productsController.ts b/src/controller/api/productsController.ts index cfe4700..e69de29 100644 --- a/src/controller/api/productsController.ts +++ b/src/controller/api/productsController.ts @@ -1,129 +0,0 @@ -// controllers/productsController.ts - -import express, { NextFunction, Request, Response } from "express"; -import { body, param } from "express-validator"; -import createHttpError from "http-errors"; -import { matchedData } from "express-validator"; -import * as productCategoriesRepository from "../../repository/productCategories"; // Repository untuk Product Categories -import * as productsRepository from "../../repository/products"; // Repository untuk Products -import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware - -// POST: Tambah produk baru -const createProduct = [ - body("product_name").notEmpty().withMessage("Product name is required"), - body("description").optional().isString(), - body("price").notEmpty().isNumeric().withMessage("Price must be a number"), - body("product_category_id").notEmpty().isInt().withMessage("Invalid category ID"), // Ganti isMongoId() ke isInt() - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const data = matchedData(req); - - // Cek apakah kategori produk ada di database - const categoryExists = await productCategoriesRepository.getProductCategoryById(data.product_category_id); - if (!categoryExists) return next(createHttpError(404, "Category not found")); - - // Simpan produk - const product = await productsRepository.createProduct(data); - - res.status(201).json({ message: "Product created", product }); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// PATCH: Perbarui produk -const updateProduct = [ - param("id").isInt().withMessage("Invalid product ID"), // Ganti isMongoId() ke isInt() - body("product_name").optional().isString(), - body("description").optional().isString(), - body("price").optional().isNumeric().withMessage("Price must be a number"), - body("product_category_id").optional().isInt().withMessage("Invalid category ID"), // Ganti isMongoId() ke isInt() - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const data = matchedData(req); - - // Jika ada perubahan kategori, cek apakah kategori tersebut ada - if (data.product_category_id) { - const categoryExists = await productCategoriesRepository.getProductCategoryById(data.product_category_id); - if (!categoryExists) return next(createHttpError(404, "Category not found")); - } - - // Update produk - const product = await productsRepository.updateProductById(Number(id), data); - if (!product) return next(createHttpError(404, "Product not found")); - - res.json({ message: "Product updated", product }); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// DELETE: Hapus produk -const deleteProduct = [ - param("id").isInt().withMessage("Invalid product ID"), // Ganti isMongoId() ke isInt() - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - - const product = await productsRepository.deleteProductById(Number(id)); - if (!product) return next(createHttpError(404, "Product not found")); - - res.json({ message: "Product deleted" }); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// GET: Ambil satu produk berdasarkan ID -const getProduct = [ - param("id").isInt().withMessage("Invalid product ID"), // Ganti isMongoId() ke isInt() - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const product = await productsRepository.getProductById(Number(id)); - - if (!product) return next(createHttpError(404, "Product not found")); - - // Ambil kategori produk terkait (join dengan kategori produk) - const productCategory = await productCategoriesRepository.getProductCategoryById(product.product_category_id); - product.product_category = productCategory; // Menambahkan informasi kategori ke dalam produk - - res.json(product); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// GET: Ambil semua produk -const getProducts = async (req: Request, res: Response, next: NextFunction) => { - try { - const allProducts = await productsRepository.getAllProducts(); - - // Ambil kategori produk terkait untuk setiap produk - for (const product of allProducts) { - const category = await productCategoriesRepository.getProductCategoryById(product.product_category_id); - product.product_category = category; - } - - res.json(allProducts); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } -}; - -export default { - createProduct, - updateProduct, - deleteProduct, - getProduct, - getProducts -}; diff --git a/src/controller/api/stockForecasting.ts b/src/controller/api/stockForecasting.ts index 469e943..917859a 100644 --- a/src/controller/api/stockForecasting.ts +++ b/src/controller/api/stockForecasting.ts @@ -2,71 +2,83 @@ import express, { NextFunction, Request, Response } from "express"; import { body, param, query, matchedData } from "express-validator"; import createHttpError from "http-errors"; import validate from "../../middleware/expressValidatorErrorHandler"; -import { predictions, products } from "../../database/models"; +import { fileValidate, multiFileValidate } from "../../middleware/fileUploader"; +import axios from "axios"; // 🟢 **POST: Tambah Prediksi Stok Baru** -const createStockForecasting = [ - body("stock_sold").notEmpty().isNumeric().withMessage("Stock sold must be a number"), - body("stock_predicted").notEmpty().isNumeric().withMessage("Stock predicted must be a number"), - body("type").notEmpty().isString().withMessage("Type is required"), - body("accuracy").optional().isNumeric().withMessage("Accuracy must be a number"), - body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"), - body("product_id").notEmpty().isMongoId().withMessage("Invalid product ID"), - validate, +// const createStockForecasting = [ +// body("stock_sold").notEmpty().isNumeric().withMessage("Stock sold must be a number"), +// body("stock_predicted").notEmpty().isNumeric().withMessage("Stock predicted must be a number"), +// body("type").notEmpty().isString().withMessage("Type is required"), +// body("accuracy").optional().isNumeric().withMessage("Accuracy must be a number"), +// body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"), +// body("product_id").notEmpty().isMongoId().withMessage("Invalid product ID"), +// validate, +// async (req: Request, res: Response, next: NextFunction) => { +// try { +// const data = matchedData(req); + +// // Cek apakah produk ada +// const productExists = await products.findById(data.product_id); +// if (!productExists) return next(createHttpError(404, "Product not found")); + +// // Simpan data prediksi +// const prediction = new predictions(data); +// await prediction.save(); +// res.status(201).json({ message: "Stock forecasting created", prediction }); +// } catch (err) { +// next(createHttpError(500, "Internal Server Error", { cause: err })); +// } +// } +// ]; + +// // 🔵 **GET: Ambil Prediksi Berdasarkan Tanggal** +// const getStockForecastingByDate = [ +// query("date").notEmpty().isISO8601().withMessage("Invalid date format"), +// validate, +// async (req: Request, res: Response, next: NextFunction) => { +// try { +// const { date } = matchedData(req); +// const startDate = new Date(date); +// const endDate = new Date(date); +// endDate.setDate(endDate.getDate() + 1); + +// const forecasts = await predictions.find({ +// createdAt: { $gte: startDate, $lt: endDate } +// }).populate("product_id user_id"); + +// if (forecasts.length === 0) return next(createHttpError(404, "No stock forecasting found for this date")); + +// res.json(forecasts); +// } catch (err) { +// next(createHttpError(500, "Internal Server Error", { cause: err })); +// } +// } +// ]; + +// // 🟠 **GET: Ambil Semua Prediksi (History)** +// const getStockForecastings = async (req: Request, res: Response, next: NextFunction) => { +// try { +// const forecasts = await predictions.find().populate("product_id user_id"); +// res.json(forecasts); +// } catch (err) { +// next(createHttpError(500, "Internal Server Error", { cause: err })); +// } +// }; + +const makePredictionFromSheet = [ + fileValidate('sheet'), async (req: Request, res: Response, next: NextFunction) => { - try { - const data = matchedData(req); - - // Cek apakah produk ada - const productExists = await products.findById(data.product_id); - if (!productExists) return next(createHttpError(404, "Product not found")); - - // Simpan data prediksi - const prediction = new predictions(data); - await prediction.save(); - res.status(201).json({ message: "Stock forecasting created", prediction }); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } + res.json({ + status: true, + message: 'File Saved' + }) } -]; - -// 🔵 **GET: Ambil Prediksi Berdasarkan Tanggal** -const getStockForecastingByDate = [ - query("date").notEmpty().isISO8601().withMessage("Invalid date format"), - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const { date } = matchedData(req); - const startDate = new Date(date); - const endDate = new Date(date); - endDate.setDate(endDate.getDate() + 1); - - const forecasts = await predictions.find({ - createdAt: { $gte: startDate, $lt: endDate } - }).populate("product_id user_id"); - - if (forecasts.length === 0) return next(createHttpError(404, "No stock forecasting found for this date")); - - res.json(forecasts); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// 🟠 **GET: Ambil Semua Prediksi (History)** -const getStockForecastings = async (req: Request, res: Response, next: NextFunction) => { - try { - const forecasts = await predictions.find().populate("product_id user_id"); - res.json(forecasts); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } -}; +] export default { - createStockForecasting, - getStockForecastingByDate, - getStockForecastings + // createStockForecasting, + // getStockForecastingByDate, + // getStockForecastings + makePredictionFromSheet }; diff --git a/src/controller/api/suppliers.ts b/src/controller/api/suppliers.ts deleted file mode 100644 index 5a17e54..0000000 --- a/src/controller/api/suppliers.ts +++ /dev/null @@ -1,101 +0,0 @@ -import express, { NextFunction, Request, Response } from "express"; -import { body, param, matchedData } from "express-validator"; -import createHttpError from "http-errors"; -import { suppliers } from "../../database/models"; -import validate from "../../middleware/expressValidatorErrorHandler"; - -// 🟢 **POST: Tambah Supplier Baru** -const createSupplier = [ - body("supplier_name").notEmpty().withMessage("Supplier name is required"), - body("contact").optional().isString().withMessage("Contact must be a string"), - body("address").optional().isString().withMessage("Address must be a string"), - body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"), - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const data = matchedData(req); - - const supplier = new suppliers(data); - await supplier.save(); - - res.status(201).json({ message: "Supplier created", supplier }); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// 🟠 **PATCH: Perbarui Supplier** -const updateSupplier = [ - param("id").isMongoId().withMessage("Invalid supplier ID"), - body("supplier_name").optional().isString(), - body("contact").optional().isString(), - body("address").optional().isString(), - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const data = matchedData(req); - - const supplier = await suppliers.findByIdAndUpdate(id, data, { new: true }); - if (!supplier) return next(createHttpError(404, "Supplier not found")); - - res.json({ message: "Supplier updated", supplier }); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// 🔴 **DELETE: Hapus Supplier** -const deleteSupplier = [ - param("id").isMongoId().withMessage("Invalid supplier ID"), - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const supplier = await suppliers.findByIdAndDelete(id); - if (!supplier) return next(createHttpError(404, "Supplier not found")); - - res.json({ message: "Supplier deleted" }); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// 🔵 **GET: Ambil Satu Supplier Berdasarkan ID** -const getSupplier = [ - param("id").isMongoId().withMessage("Invalid supplier ID"), - validate, - async (req: Request, res: Response, next: NextFunction) => { - try { - const { id } = req.params; - const supplier = await suppliers.findById(id).populate("user_id"); - - if (!supplier) return next(createHttpError(404, "Supplier not found")); - - res.json(supplier); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } - } -]; - -// 🟡 **GET: Ambil Semua Supplier** -const getSuppliers = async (req: Request, res: Response, next: NextFunction) => { - try { - const allSuppliers = await suppliers.find().populate("user_id"); - res.json(allSuppliers); - } catch (err) { - next(createHttpError(500, "Internal Server Error", { cause: err })); - } -}; - -export default { - createSupplier, - updateSupplier, - deleteSupplier, - getSupplier, - getSuppliers -}; diff --git a/src/controller/api/suppliersController.ts b/src/controller/api/suppliersController.ts new file mode 100644 index 0000000..f565a2f --- /dev/null +++ b/src/controller/api/suppliersController.ts @@ -0,0 +1,121 @@ +import { body, matchedData, param, query } from "express-validator" +import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler" +import { NextFunction, Request, Response } from "express" +import { createSupplier, deleteSupplier, getSupplier, getSuppliers, updateSupplier } from "../../services/supplierServices" +import { TAPIResponse } from "../../types/core/http" +import { ISupplierTable } from "../../types/db-model" +import createHttpError from "http-errors" +import parsePhoneNumberFromString from "libphonenumber-js" + +const addSupplier = [ + body('supplier_name').notEmpty().isString(), + body('contact') + .optional({ nullable: true }) + .isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'), + body('address').optional().isString(), + expressValidatorErrorHandler, + async (req: Request, res: Response, next: NextFunction) => { + const reqData = matchedData(req) + const phone = parsePhoneNumberFromString(`+${reqData.contact}`); + if (!phone?.isValid()) + next(createHttpError(400, 'Invalid phone number format. Please match international format.')) + try { + const newSupplierId = await createSupplier({ + ...reqData, + user_id: req.user!.id + }) + const result: TAPIResponse = { + success: true, + message: 'New supplier successfully created.', + data: { + supplierId: newSupplierId + } + } + res.json(result) + } catch (error) { + next(createHttpError(500, error as Error)) + } + } +] + +const showAllSupplier = [ + query('page').optional().isInt(), + query('limit').optional().isInt(), + async (req: Request, res: Response, next: NextFunction) => { + try { + const { page = 1, limit = 10 } = matchedData(req) + const suppliers = await getSuppliers(req.user!.id, page, limit); + const result: TAPIResponse = { + success: true, + data: suppliers + } + res.json(result) + } catch (error) { + next(createHttpError(500, error as Error)) + } + } +] + +const getSupplierDetail = [ + param('id'), + expressValidatorErrorHandler, + async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = matchedData(req) + const supplier = await getSupplier(id) + const result: TAPIResponse = { + success: true, + data: supplier + } + res.json(result) + } catch (error) { + next(createHttpError(500, error as Error)) + } + } +] + +const updateSupplierRoute = [ + param('id'), + body('supplier_name').notEmpty().isString(), + body('contact') + .optional({ nullable: true }) + .isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'), + body('address').optional().isString(), + expressValidatorErrorHandler, + async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = matchedData(req, { locations: ['params'] }) + const reqBody = matchedData(req, { locations: ['body'] }) + const phone = parsePhoneNumberFromString(`+${reqBody.contact}`); + if (!phone?.isValid()) + next(createHttpError(400, 'Invalid phone number format. Please match international format.')) + await updateSupplier(id, reqBody) + const result: TAPIResponse = { + success: true, + message: 'Supplier data successfully updated' + } + res.json(result) + } catch (error) { + next(createHttpError(500, error as Error)) + } + } +] + +const deleteSupplierRoute = [ + param('id'), + expressValidatorErrorHandler, + async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = matchedData(req) + await deleteSupplier(id) + const result: TAPIResponse = { + success: true, + message: 'Supplier successfully deleted' + } + res.json(result) + } catch (error) { + next(createHttpError(500, error as Error)) + } + } +] +export default { addSupplier, showAllSupplier, getSupplierDetail, updateSupplierRoute, deleteSupplierRoute } \ No newline at end of file diff --git a/src/middleware/fileUploader.ts b/src/middleware/fileUploader.ts new file mode 100644 index 0000000..ff956e8 --- /dev/null +++ b/src/middleware/fileUploader.ts @@ -0,0 +1,62 @@ +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import { root_path } from '../utils/core/storage'; + +const allowedExtensions = ['.xlsx', '.xls', '.csv', '.tsv', '.ods', '.json', '.txt', '.html', '.slk', '.dbf', '.prn', '.dif']; +const allowedMimeTypes = [ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'text/csv', + 'application/vnd.oasis.opendocument.spreadsheet' +]; + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + const uploadDir = root_path('src/temp'); + if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); + cb(null, uploadDir); + }, + filename: function (req, file, cb) { + const ext = path.extname(file.originalname); + const base = path.basename(file.originalname, ext); + const timestamp = Date.now(); + cb(null, `${base}-${timestamp}${ext}`); + } +}); + +export function fileValidate(fieldName: string) { + return multer({ + storage, + limits: { fileSize: 10 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + const isValidMime = allowedMimeTypes.includes(file.mimetype); + const isValidExt = allowedExtensions.includes(ext); + + if (!isValidMime || !isValidExt) { + return cb(new Error('Unsupported file format for SheetJS') as any, false); + } + + cb(null, true); + } + }).single(fieldName); +} + +export function multiFileValidate(fields: multer.Field[]) { + return multer({ + storage, + limits: { fileSize: 10 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + const isValidMime = allowedMimeTypes.includes(file.mimetype); + const isValidExt = allowedExtensions.includes(ext); + + if (!isValidMime || !isValidExt) { + return cb(new Error('Unsupported file format for SheetJS') as any, false); + } + + cb(null, true); + } + }).fields(fields); +} diff --git a/src/repository/suppliersRepository.ts b/src/repository/suppliersRepository.ts new file mode 100644 index 0000000..d7cb491 --- /dev/null +++ b/src/repository/suppliersRepository.ts @@ -0,0 +1,44 @@ +import { db } from "../database/MySQL" +import { ISupplierTable } from "../types/db-model" + +export const insertSupplier = async ({ + address, contact, supplier_name, user_id +}: ISupplierTable) => { + const [id] = await db('suppliers').insert({ + supplier_name, contact, user_id, address + }) + return id +} + +export const getSupplierById = (id: number) => { + return db('suppliers').where({ id }) +} + +export const getAllSupplier = (user_id: number, limit: number, offset: number) => { + return db('suppliers') + .select('*') + .where({ user_id }) + .limit(limit) + .offset(offset); +} + + +export const countSuppliers = async (id_user: number) => { + const result = await db('suppliers') + .where({ user_id: id_user }) + .count('id as count') + .first<{ + count: string; + }>(); + + return parseInt(result?.count || '0'); +}; + + +export const updateSupplierById = (id: number, data: ISupplierTable) => { + return getSupplierById(id).update(data) +} + +export const deleteSupplierById = (id: number) => { + return getSupplierById(id).delete() +} diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts index f4b0beb..3736514 100644 --- a/src/routes/api/index.ts +++ b/src/routes/api/index.ts @@ -2,6 +2,7 @@ import express from 'express' var router = express.Router(); import authEndpoint from './auth' +import supplierEndpoint from './supplier' import authenticate from '../../middleware/authMiddleware'; // import productCategory from '../../controller/api/productCategoriesController'; // import product from '../../controller/api/productsController'; @@ -25,56 +26,6 @@ router.get('/is-authenticated', (req, res) => { }) }) -// ----- Product Categories -// router.post("/product-category", productCategory.createCategory); -// router.get("/product-categories", productCategory.getCategories); -// router.get("/product-category/:id", productCategory.getCategory); -// router.patch("/product-category/:id", productCategory.updateCategory); -// router.delete("/product-category/:id", productCategory.deleteCategory); - - -// ----- Products -// router.get("/product/:id", product.getProduct); -// router.post("/product", product.createProduct); -// router.patch("/product/:id", product.updateProduct); -// router.delete("/product/:id", product.deleteProduct); -// router.get("/products", product.getProducts); - - -// ----- Stock Forecasting -// router.post("/stock-forecasting", stockForecastingController.createStockForecasting); -// router.get("/stock-forecasting", stockForecastingController.getStockForecastingByDate); -// router.get("/stock-forecasting/history", stockForecastingController.getStockForecastings); - - -// ----- Stock Purchases -// router.post("/stock-purchase", stockPurchaseController.createStockPurchase); -// router.patch("/stock-purchase/:id", stockPurchaseController.updateStockPurchase); -// router.delete("/stock-purchase/:id", stockPurchaseController.deleteStockPurchase); -// router.get("/stock-purchase/:id", stockPurchaseController.getStockPurchase); -// router.get("/stock-purchase", stockPurchaseController.getStockPurchases); - - -// ----- Suppliers -// router.post("/supplier", supplierController.createSupplier); -// router.patch("/supplier/:id", supplierController.updateSupplier); -// router.delete("/supplier/:id", supplierController.deleteSupplier); -// router.get("/supplier/:id", supplierController.getSupplier); -// router.get("/supplier", supplierController.getSuppliers); - - -// ----- Transactions -// router.post("/transaction", transactionController.createTransaction); -// router.patch("/transaction/:id", transactionController.updateTransaction); -// router.delete("/transaction/:id", transactionController.deleteTransaction); -// router.get("/transaction/:id", transactionController.getTransaction); -// router.get("/transaction", transactionController.getTransactions); - -// ----- userFile -// router.post("/user-files", userFileController.createUserFile); -// router.patch("/user-files/:id", userFileController.updateUserFile); -// router.delete("/user-files/:id", userFileController.deleteUserFile); -// router.get("/user-files/:id", userFileController.getUserFile); -// router.get("/user-files", userFileController.getUserFiles); +router.use('/', supplierEndpoint) export default router; diff --git a/src/routes/api/prediction.ts b/src/routes/api/prediction.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/api/supplier.ts b/src/routes/api/supplier.ts new file mode 100644 index 0000000..4118b9d --- /dev/null +++ b/src/routes/api/supplier.ts @@ -0,0 +1,13 @@ +import express from 'express' +var router = express.Router(); +import supplierController from '../../controller/api/suppliersController'; +import authenticate from '../../middleware/authMiddleware'; + +router.use('/', authenticate) +router.post('/supplier', supplierController.addSupplier); +router.get('/suppliers', supplierController.showAllSupplier); +router.get('/supplier/:id', supplierController.getSupplierDetail); +router.patch('/supplier/:id', supplierController.updateSupplierRoute); +router.delete('/supplier/:id', supplierController.deleteSupplierRoute); + +export default router \ No newline at end of file diff --git a/src/services/supplierServices.ts b/src/services/supplierServices.ts new file mode 100644 index 0000000..254c0c9 --- /dev/null +++ b/src/services/supplierServices.ts @@ -0,0 +1,31 @@ +import { countSuppliers, deleteSupplierById, getAllSupplier, getSupplierById, insertSupplier, updateSupplierById } from "../repository/suppliersRepository"; +import { ISupplierTable } from "../types/db-model"; + +export const createSupplier = async (data: ISupplierTable) => insertSupplier(data) +export const getSuppliers = async ( + id_user: number, + page = 1, + limit = 10 +) => { + const safePage = Math.max(1, Number(page)); + const safeLimit = Math.max(1, Number(limit)); + const offset = (safePage - 1) * safeLimit; + + const [data, total] = await Promise.all([ + getAllSupplier(id_user, safeLimit, offset), + countSuppliers(id_user) + ]); + + return { + data, + meta: { + page: safePage, + limit: safeLimit, + total, + totalPages: Math.ceil(total / safeLimit) + } + }; +}; +export const getSupplier = async (id: number) => getSupplierById(id).first() +export const updateSupplier = async (id: number, data: ISupplierTable) => updateSupplierById(id, data) +export const deleteSupplier = async (id: number) => deleteSupplierById(id) \ No newline at end of file diff --git a/src/temp/dummy_time_series-1747066831499.xlsx b/src/temp/dummy_time_series-1747066831499.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1c9ae46cd59923c4ac452f2458e9b45b419d4ab8 GIT binary patch literal 10086 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 = { data?: T; }; -export type TPaginatedResponse> = TAPIResponse & { - pagination?: { - total: number; +interface TPaginatedResult { + data: T[]; + meta: { page: number; - pageSize: number; + limit: number; + total: number; totalPages: number; - } -}; \ No newline at end of file + }; +} + +export type TPaginatedResponse> = + TAPIResponse>