menambahkan crud suppliers
This commit is contained in:
parent
f5639c6615
commit
cd198e9272
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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<ISupplierTable>(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<ISupplierTable>(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 }
|
|
@ -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);
|
||||
}
|
|
@ -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<ISupplierTable>('suppliers').insert({
|
||||
supplier_name, contact, user_id, address
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
export const getSupplierById = (id: number) => {
|
||||
return db<ISupplierTable>('suppliers').where({ id })
|
||||
}
|
||||
|
||||
export const getAllSupplier = (user_id: number, limit: number, offset: number) => {
|
||||
return db<ISupplierTable>('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()
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|
@ -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)
|
Binary file not shown.
|
@ -5,6 +5,8 @@ declare namespace NodeJS {
|
|||
PORT: string;
|
||||
ALLOWED_ORIGIN: string;
|
||||
|
||||
PYTHON_API_HOST: string;
|
||||
|
||||
DB_CONNECTION: 'mysql' | string;
|
||||
DB_HOST: string;
|
||||
DB_PORT: string;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { IUser } from "../../db-model";
|
||||
import { TAccessToken } from "../../jwt";
|
||||
import { TAPIResponse, TPaginatedResponse } from "../http";
|
||||
|
||||
declare module "express-serve-static-core" {
|
||||
interface Request {
|
||||
|
|
|
@ -5,11 +5,15 @@ export type TAPIResponse<T = Record<string, any>> = {
|
|||
data?: T;
|
||||
};
|
||||
|
||||
export type TPaginatedResponse<T = Record<string, any>> = TAPIResponse<T> & {
|
||||
pagination?: {
|
||||
total: number;
|
||||
interface TPaginatedResult<T> {
|
||||
data: T[];
|
||||
meta: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type TPaginatedResponse<T = Record<string, any>> =
|
||||
TAPIResponse<TPaginatedResult<T>>
|
||||
|
|
Loading…
Reference in New Issue