fix 1
This commit is contained in:
parent
da539298fb
commit
ecb30ae790
|
@ -35,7 +35,8 @@
|
|||
"node-schedule": "^2.1.1",
|
||||
"nodemailer": "^6.10.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"pg": "^8.11.1"
|
||||
"pg": "^8.11.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
|
@ -694,6 +695,15 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
|
@ -1062,6 +1072,19 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
@ -1111,6 +1134,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
@ -1330,6 +1362,18 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
|
@ -1793,6 +1837,15 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
|
@ -3886,6 +3939,18 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
@ -4311,12 +4376,51 @@
|
|||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
"node-schedule": "^2.1.1",
|
||||
"nodemailer": "^6.10.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"pg": "^8.11.1"
|
||||
"pg": "^8.11.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
|
|
|
@ -191,13 +191,13 @@ const login = [
|
|||
await updateUserById(user.id, {
|
||||
refresh_token: refreshToken
|
||||
})
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
// Kirim token sebagai cookie
|
||||
res.cookie("refreshToken", refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 15 * 24 * 60 * 60 * 1000
|
||||
secure: isProd,
|
||||
sameSite: isProd ? 'none' : 'lax',
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000
|
||||
});
|
||||
|
||||
const resultResponse: TAPIResponse = {
|
||||
|
|
|
@ -36,7 +36,6 @@ const getProductCategories = [
|
|||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const productCategories = await showAllProductCategory(req.user!.id)
|
||||
console.log(productCategories)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: productCategories
|
||||
|
|
|
@ -38,7 +38,8 @@ const addProduct = [
|
|||
success: true,
|
||||
message: 'New product successfully created.',
|
||||
data: {
|
||||
productId: newProductId
|
||||
...reqData,
|
||||
id: newProductId,
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
|
@ -86,8 +87,8 @@ const getProductDetail = [
|
|||
|
||||
const updateProductRoute = [
|
||||
param('id').notEmpty(),
|
||||
body('product_code').notEmpty().isString(),
|
||||
body('product_name').notEmpty().isString(),
|
||||
body('product_code').optional({ values: 'falsy' }).isString(),
|
||||
body('product_name').optional({ values: 'falsy' }).isString(),
|
||||
body('stock')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
|
@ -105,16 +106,11 @@ const updateProductRoute = [
|
|||
try {
|
||||
const { id } = matchedData(req, { locations: ['params'] })
|
||||
const reqBody = matchedData<IProductTable>(req, { locations: ['body'] })
|
||||
await updateProductById(id, {
|
||||
...reqBody,
|
||||
buying_price: reqBody.buying_price || null,
|
||||
selling_price: reqBody.selling_price || null,
|
||||
stock: reqBody.stock || 0,
|
||||
product_category_id: reqBody.product_category_id || null,
|
||||
})
|
||||
await updateProductById(id, reqBody)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Product data successfully updated'
|
||||
message: 'Product data successfully updated',
|
||||
data: await showProductById(id)
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
import express, { NextFunction, Request, Response } from "express";
|
||||
import { body, param, matchedData } from "express-validator";
|
||||
import createHttpError from "http-errors";
|
||||
import { purchases, products, suppliers } from "../../database/models";
|
||||
import validate from "../../middleware/expressValidatorErrorHandler";
|
||||
|
||||
// 🟢 **POST: Tambah Pembelian Stok Baru**
|
||||
const createStockPurchase = [
|
||||
body("amount").notEmpty().isNumeric().withMessage("Amount must be a number"),
|
||||
body("total_price").notEmpty().isNumeric().withMessage("Total price must be a number"),
|
||||
body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"),
|
||||
body("product_id").notEmpty().isMongoId().withMessage("Invalid product ID"),
|
||||
body("supplier_id").notEmpty().isMongoId().withMessage("Invalid supplier 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"));
|
||||
|
||||
// Cek apakah supplier ada
|
||||
const supplierExists = await suppliers.findById(data.supplier_id);
|
||||
if (!supplierExists) return next(createHttpError(404, "Supplier not found"));
|
||||
|
||||
// Simpan data pembelian
|
||||
const purchase = new purchases(data);
|
||||
await purchase.save();
|
||||
res.status(201).json({ message: "Stock purchase created", purchase });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🟠 **PATCH: Perbarui Pembelian Stok**
|
||||
const updateStockPurchase = [
|
||||
param("id").isMongoId().withMessage("Invalid purchase ID"),
|
||||
body("amount").optional().isNumeric().withMessage("Amount must be a number"),
|
||||
body("total_price").optional().isNumeric().withMessage("Total price must be a number"),
|
||||
body("supplier_id").optional().isMongoId().withMessage("Invalid supplier ID"),
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = matchedData(req);
|
||||
|
||||
if (data.supplier_id) {
|
||||
const supplierExists = await suppliers.findById(data.supplier_id);
|
||||
if (!supplierExists) return next(createHttpError(404, "Supplier not found"));
|
||||
}
|
||||
|
||||
const purchase = await purchases.findByIdAndUpdate(id, data, { new: true });
|
||||
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
|
||||
|
||||
res.json({ message: "Stock purchase updated", purchase });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🔴 **DELETE: Hapus Pembelian Stok**
|
||||
const deleteStockPurchase = [
|
||||
param("id").isMongoId().withMessage("Invalid purchase ID"),
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const purchase = await purchases.findByIdAndDelete(id);
|
||||
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
|
||||
|
||||
res.json({ message: "Stock purchase deleted" });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🔵 **GET: Ambil Satu Pembelian Stok Berdasarkan ID**
|
||||
const getStockPurchase = [
|
||||
param("id").isMongoId().withMessage("Invalid purchase ID"),
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const purchase = await purchases.findById(id).populate("product_id supplier_id user_id");
|
||||
|
||||
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
|
||||
|
||||
res.json(purchase);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🟡 **GET: Ambil Semua Pembelian Stok (Histori)**
|
||||
const getStockPurchases = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const allPurchases = await purchases.find().populate("product_id supplier_id user_id");
|
||||
res.json(allPurchases);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createStockPurchase,
|
||||
updateStockPurchase,
|
||||
deleteStockPurchase,
|
||||
getStockPurchase,
|
||||
getStockPurchases
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
import { body, matchedData, query } from "express-validator";
|
||||
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { TAPIResponse } from "../../types/core/http";
|
||||
import { addRestocksRecords, showRestockHistory } from "../../services/restockServices";
|
||||
import createHttpError from "http-errors";
|
||||
import { addTransactionRecords, showSalesHistory } from "../../services/transactionServices";
|
||||
|
||||
const createRestockRecord = [
|
||||
body('data').isArray().withMessage('Body must be array'),
|
||||
body('data.*.product_code')
|
||||
.isString().withMessage('product_code must be string')
|
||||
.notEmpty().withMessage('product_code is required'),
|
||||
body('data.*.product_name')
|
||||
.isString().withMessage('product_name must be string')
|
||||
.notEmpty().withMessage('product_name is required'),
|
||||
body('data.*.price')
|
||||
.isNumeric().withMessage('price must be number'),
|
||||
body('data.*.amount')
|
||||
.isNumeric().withMessage('amount must be number'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { data }: {
|
||||
data: {
|
||||
product_code: string;
|
||||
product_name: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
}[]
|
||||
} = matchedData(req, { locations: ['body'] });
|
||||
try {
|
||||
await addRestocksRecords(data, req.user!.id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Restock successful.',
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const createTransactionRecord = [
|
||||
body('data').isArray().withMessage('Body must be array'),
|
||||
body('data.*.product_code')
|
||||
.isString().withMessage('product_code must be string')
|
||||
.notEmpty().withMessage('product_code is required'),
|
||||
body('data.*.product_name')
|
||||
.isString().withMessage('product_name must be string')
|
||||
.notEmpty().withMessage('product_name is required'),
|
||||
body('data.*.price')
|
||||
.isNumeric().withMessage('price must be number'),
|
||||
body('data.*.amount')
|
||||
.isNumeric().withMessage('amount must be number'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { data }: {
|
||||
data: {
|
||||
product_code: string;
|
||||
product_name: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
}[]
|
||||
} = matchedData(req, { locations: ['body'] });
|
||||
try {
|
||||
await addTransactionRecords(data, req.user!.id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Transaction successful.',
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const showRestockHistoryRoute = [
|
||||
query('page').optional().isInt(),
|
||||
query('limit').optional().isInt(),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { page = 1, limit = 10 } = matchedData(req)
|
||||
const restockHistory = await showRestockHistory(req.user!.id, page, limit)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: restockHistory
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const showSalesHistoryRoute = [
|
||||
query('page').optional().isInt(),
|
||||
query('limit').optional().isInt(),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { page = 1, limit = 10 } = matchedData(req)
|
||||
const restockHistory = await showSalesHistory(req.user!.id, page, limit)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: restockHistory
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
createRestockRecord,
|
||||
createTransactionRecord,
|
||||
showRestockHistoryRoute,
|
||||
showSalesHistoryRoute
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
// controllers/transactionsController.ts
|
||||
|
||||
import { body, param, matchedData } from "express-validator";
|
||||
import createHttpError from "http-errors";
|
||||
import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware
|
||||
import * as transactionsRepository from "../../repository/transaction"; // Repository untuk Transactions dalam bentuk mongoose
|
||||
import * as productsRepository from "../../repository/productsRepository"; // Untuk cek produk
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { ITransaction } from "../../types/db-model";
|
||||
|
||||
// 🟢 **POST: Tambah Transaksi Baru**
|
||||
const createTransaction = [
|
||||
body("amount").notEmpty().isNumeric().withMessage("Amount must be a number"),
|
||||
body("total_price").notEmpty().isNumeric().withMessage("Total price must be a number"),
|
||||
body("user_id").notEmpty().isInt().withMessage("Invalid user ID"), // isMongoId() diganti isInt()
|
||||
body("product_id").notEmpty().isInt().withMessage("Invalid product ID"), // isMongoId() diganti isInt()
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = matchedData<ITransaction>(req);
|
||||
|
||||
// Cek apakah produk ada
|
||||
const productExists = await productsRepository.getProductById(data.product_id);
|
||||
if (!productExists) return next(createHttpError(404, "Product not found"));
|
||||
|
||||
// Simpan data transaksi
|
||||
const transaction = await transactionsRepository.createTransaction(data);
|
||||
|
||||
res.status(201).json({ message: "Transaction created", transaction });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🟠 **PATCH: Perbarui Transaksi**
|
||||
const updateTransaction = [
|
||||
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
|
||||
body("amount").optional().isNumeric().withMessage("Amount must be a number"),
|
||||
body("total_price").optional().isNumeric().withMessage("Total price must be a number"),
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = matchedData<ITransaction>(req);
|
||||
|
||||
const transaction = await transactionsRepository.updateTransactionById(Number(id), data);
|
||||
if (!transaction) return next(createHttpError(404, "Transaction not found"));
|
||||
|
||||
res.json({ message: "Transaction updated", transaction });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🔴 **DELETE: Hapus Transaksi**
|
||||
const deleteTransaction = [
|
||||
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const transaction = await transactionsRepository.deleteTransactionById(Number(id));
|
||||
if (!transaction) return next(createHttpError(404, "Transaction not found"));
|
||||
|
||||
res.json({ message: "Transaction deleted" });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🔵 **GET: Ambil Satu Transaksi Berdasarkan ID**
|
||||
const getTransaction = [
|
||||
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const transaction = await transactionsRepository.getTransactionById(Number(id));
|
||||
|
||||
if (!transaction) return next(createHttpError(404, "Transaction not found"));
|
||||
|
||||
res.json(transaction);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🟡 **GET: Ambil Semua Transaksi**
|
||||
const getTransactions = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const allTransactions = await transactionsRepository.getAllTransactions();
|
||||
res.json(allTransactions);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createTransaction,
|
||||
updateTransaction,
|
||||
deleteTransaction,
|
||||
getTransaction,
|
||||
getTransactions
|
||||
};
|
|
@ -8,7 +8,6 @@ const authenticate: RequestHandler = (req, res, next) => {
|
|||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return next(createHttpError(401, "Access token required"));
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1];
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
|
||||
|
|
|
@ -55,4 +55,10 @@ export const selectProductByProductCode = (product_code: IProductTable['product_
|
|||
product_code,
|
||||
user_id,
|
||||
})
|
||||
}
|
||||
|
||||
export const selectProductByProductCodes = (product_codes: IProductTable['product_code'][], user_id: IProductTable['user_id']) => {
|
||||
return db<IProductTable>('products').where({
|
||||
user_id,
|
||||
}).whereIn('product_code', product_codes)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { IPurchaseTable } from "../types/db-model";
|
||||
|
||||
export function insertsDataToRestock(data: Partial<IPurchaseTable>[]) {
|
||||
return db<IPurchaseTable>('restocks').insert(data)
|
||||
}
|
||||
|
||||
export function selectAllRestockHistory(
|
||||
user_id: IPurchaseTable['user_id'],
|
||||
limit: number, offset: number
|
||||
) {
|
||||
return db<IPurchaseTable>('restocks as r')
|
||||
.leftJoin('products as p', 'p.id', 'r.product_id')
|
||||
.select('r.id', 'p.product_name', 'r.buying_date', 'r.price', 'r.amount', 'r.total_price')
|
||||
.where('p.user_id', user_id)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
}
|
||||
|
||||
export const countRestocks = async (id_user: number) => {
|
||||
const result = await db('restocks')
|
||||
.where({ user_id: id_user })
|
||||
.count('id as count')
|
||||
.first<{
|
||||
count: string;
|
||||
}>();
|
||||
|
||||
return parseInt(result?.count || '0');
|
||||
};
|
|
@ -22,7 +22,6 @@ export const getAllSupplier = (user_id: number, limit: number, offset: number) =
|
|||
.offset(offset);
|
||||
}
|
||||
|
||||
|
||||
export const countSuppliers = async (id_user: number) => {
|
||||
const result = await db('suppliers')
|
||||
.where({ user_id: id_user })
|
||||
|
@ -34,7 +33,6 @@ export const countSuppliers = async (id_user: number) => {
|
|||
return parseInt(result?.count || '0');
|
||||
};
|
||||
|
||||
|
||||
export const updateSupplierById = (id: number, data: ISupplierTable) => {
|
||||
return getSupplierById(id).update(data)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { ITransactionTable } from "../types/db-model";
|
||||
|
||||
export function insertsDataToTransaction(data: Partial<ITransactionTable>[]) {
|
||||
return db<ITransactionTable>('transactions').insert(data)
|
||||
}
|
||||
|
||||
export function selectAllSalesHistory(
|
||||
user_id: ITransactionTable['user_id'],
|
||||
limit: number, offset: number
|
||||
) {
|
||||
return db<ITransactionTable>('transactions as t')
|
||||
.leftJoin('products as p', 'p.id', 't.product_id')
|
||||
.select('t.id', 'p.product_name', 't.transaction_date', 't.price', 't.amount', 't.total_price')
|
||||
.where('p.user_id', user_id)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
}
|
||||
|
||||
export const countSales = async (id_user: number) => {
|
||||
const result = await db('transactions')
|
||||
.where({ user_id: id_user })
|
||||
.count('id as count')
|
||||
.first<{
|
||||
count: string;
|
||||
}>();
|
||||
|
||||
return parseInt(result?.count || '0');
|
||||
};
|
|
@ -11,6 +11,6 @@ router.patch('/forgot-password/:token', authRoute.forgotPasswordChangePassword);
|
|||
router.get('/verify/:token', authRoute.verify);
|
||||
router.post('/re-send-email-activation/:token', authRoute.resendEmailVerification);
|
||||
router.get('/refresh-token', authRoute.refreshToken);
|
||||
router.get('/logout', authenticate, authRoute.logout);
|
||||
router.get('/logout', authRoute.logout);
|
||||
|
||||
export default router
|
|
@ -4,6 +4,7 @@ var router = express.Router();
|
|||
import authEndpoint from './auth'
|
||||
import supplierEndpoint from './supplier'
|
||||
import productCategoryEndpoint from './product_category'
|
||||
import trxEndpoint from './trx'
|
||||
import productEndpoint from './product'
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
|
@ -23,5 +24,6 @@ router.get('/is-authenticated', (req, res) => {
|
|||
router.use('/', supplierEndpoint)
|
||||
router.use('/', productEndpoint)
|
||||
router.use('/', productCategoryEndpoint)
|
||||
router.use('/', trxEndpoint)
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import express from 'express'
|
||||
var router = express.Router();
|
||||
import transactionController from '../../controller/api/transactionController';
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.use('/', authenticate)
|
||||
router.post('/restocks', transactionController.createRestockRecord);
|
||||
router.post('/transactions', transactionController.createTransactionRecord);
|
||||
router.get('/restocks-history', transactionController.showRestockHistoryRoute);
|
||||
router.get('/sales-history', transactionController.showSalesHistoryRoute);
|
||||
|
||||
export default router
|
|
@ -1,4 +1,4 @@
|
|||
import { countProducts, getAllProducts, insertProduct, selectProductById, selectProductByProductCode, selectProductsByUserId } from "../repository/productsRepository"
|
||||
import { countProducts, getAllProducts, insertProduct, selectProductById, selectProductByProductCode, selectProductByProductCodes, selectProductsByUserId } from "../repository/productsRepository"
|
||||
import { IProductTable } from "../types/db-model"
|
||||
|
||||
export const createProduct = async (data: IProductTable) => {
|
||||
|
@ -45,4 +45,9 @@ export const deleteProductById = async (id: IProductTable['id']) => {
|
|||
export const getProductByProductCode = (
|
||||
user_id: IProductTable['user_id'],
|
||||
product_code: IProductTable['product_code']
|
||||
) => selectProductByProductCode(product_code, user_id).first()
|
||||
) => selectProductByProductCode(product_code, user_id).first()
|
||||
|
||||
export const getProductByProductCodes = (
|
||||
user_id: IProductTable['user_id'],
|
||||
product_codes: IProductTable['product_code'][]
|
||||
) => selectProductByProductCodes(product_codes, user_id)
|
|
@ -0,0 +1,54 @@
|
|||
import { countRestocks, insertsDataToRestock, selectAllRestockHistory } from "../repository/restockRepository";
|
||||
import { IProductTable, IPurchaseTable } from "../types/db-model";
|
||||
import { getProductByProductCodes, updateProductById } from "./productServices";
|
||||
|
||||
export async function addRestocksRecords(data: {
|
||||
product_code: string;
|
||||
product_name: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
}[], user_id: IProductTable['user_id']) {
|
||||
const product_codes = data.map(v => v.product_code)
|
||||
const products = await getProductByProductCodes(user_id, product_codes)
|
||||
const mergedData: Partial<IPurchaseTable>[] = []
|
||||
products.forEach((p) => {
|
||||
const pCode = p.product_code
|
||||
const dataMatched = data.find(v => v.product_code === pCode)
|
||||
mergedData.push({
|
||||
amount: dataMatched!.amount,
|
||||
product_id: p.id,
|
||||
user_id,
|
||||
price: Number(p.buying_price),
|
||||
})
|
||||
updateProductById(p.id, {
|
||||
stock: p.stock + (Number(dataMatched?.amount) || 0)
|
||||
})
|
||||
})
|
||||
return insertsDataToRestock(mergedData)
|
||||
}
|
||||
|
||||
export async function showRestockHistory(
|
||||
user_id: IPurchaseTable['user_id'],
|
||||
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([
|
||||
selectAllRestockHistory(user_id, safeLimit, offset),
|
||||
countRestocks(user_id)
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
page: safePage,
|
||||
limit: safeLimit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / safeLimit)
|
||||
}
|
||||
};
|
||||
return
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { countSales, insertsDataToTransaction, selectAllSalesHistory } from "../repository/transactionRepository";
|
||||
import { IProductTable, ITransactionTable } from "../types/db-model";
|
||||
import { getProductByProductCodes, updateProductById } from "./productServices";
|
||||
|
||||
export async function addTransactionRecords(data: {
|
||||
product_code: string;
|
||||
product_name: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
}[], user_id: IProductTable['user_id']) {
|
||||
const product_codes = data.map(v => v.product_code)
|
||||
const products = await getProductByProductCodes(user_id, product_codes)
|
||||
const mergedData: Partial<ITransactionTable>[] = []
|
||||
products.forEach((p) => {
|
||||
const pCode = p.product_code
|
||||
const dataMatched = data.find(v => v.product_code === pCode)
|
||||
mergedData.push({
|
||||
amount: dataMatched!.amount,
|
||||
product_id: p.id,
|
||||
user_id,
|
||||
price: Number(p.selling_price),
|
||||
})
|
||||
|
||||
updateProductById(p.id, {
|
||||
stock: (Number(dataMatched?.amount) || 0) - p.stock
|
||||
})
|
||||
})
|
||||
return insertsDataToTransaction(mergedData)
|
||||
}
|
||||
|
||||
export async function showSalesHistory(
|
||||
user_id: ITransactionTable['user_id'],
|
||||
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([
|
||||
selectAllSalesHistory(user_id, safeLimit, offset),
|
||||
countSales(user_id)
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
page: safePage,
|
||||
limit: safeLimit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / safeLimit)
|
||||
}
|
||||
};
|
||||
return
|
||||
}
|
|
@ -39,6 +39,7 @@ export interface IPurchaseTable {
|
|||
id: number;
|
||||
buying_date: Date;
|
||||
amount: number;
|
||||
price: number;
|
||||
total_price: number;
|
||||
user_id: number;
|
||||
product_id: number;
|
||||
|
@ -52,6 +53,7 @@ export interface ITransactionTable {
|
|||
total_price: number;
|
||||
user_id: number;
|
||||
product_id: number;
|
||||
price: number
|
||||
}
|
||||
|
||||
export interface IPredictionTable {
|
||||
|
|
|
@ -4,11 +4,13 @@ import { TAccessToken, TRefreshToken } from '../../types/jwt';
|
|||
import { IUserTable } from '../../types/db-model';
|
||||
dotenv.config()
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const generateAccessToken = (user: IUserTable) => {
|
||||
return jwt.sign(
|
||||
{ id: user.id, email: user.email } as TAccessToken,
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: "15m" }
|
||||
{ expiresIn: isProd ? "15m" : '30d' }
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue