menambahkan crud suppliers

This commit is contained in:
Rynare 2025-05-15 09:07:07 +07:00
parent f5639c6615
commit cd198e9272
16 changed files with 456 additions and 351 deletions

97
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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 }

View File

@ -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);
}

View File

@ -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()
}

View File

@ -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;

View File

View File

@ -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

View File

@ -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.

View File

@ -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;

View File

@ -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 {

View File

@ -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>>