diff --git a/package-lock.json b/package-lock.json index 25769c0..bf608bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bdec4de..78c51c2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/controller/api/authController.ts b/src/controller/api/authController.ts index ce8dc19..04d5cc8 100644 --- a/src/controller/api/authController.ts +++ b/src/controller/api/authController.ts @@ -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 = { diff --git a/src/controller/api/productCategoriesController.ts b/src/controller/api/productCategoriesController.ts index 2914d88..43b0238 100644 --- a/src/controller/api/productCategoriesController.ts +++ b/src/controller/api/productCategoriesController.ts @@ -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 diff --git a/src/controller/api/productsController.ts b/src/controller/api/productsController.ts index a946896..8868c30 100644 --- a/src/controller/api/productsController.ts +++ b/src/controller/api/productsController.ts @@ -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(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) { diff --git a/src/controller/api/stockPurchases.ts b/src/controller/api/stockPurchases.ts deleted file mode 100644 index 3d93f2e..0000000 --- a/src/controller/api/stockPurchases.ts +++ /dev/null @@ -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 -}; diff --git a/src/controller/api/transactionController.ts b/src/controller/api/transactionController.ts new file mode 100644 index 0000000..2cbe510 --- /dev/null +++ b/src/controller/api/transactionController.ts @@ -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 +} \ No newline at end of file diff --git a/src/controller/api/transactions.ts b/src/controller/api/transactions.ts deleted file mode 100644 index d32e961..0000000 --- a/src/controller/api/transactions.ts +++ /dev/null @@ -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(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(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 -}; diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 9fb2bbf..ed18fe8 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -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) => { diff --git a/src/repository/productsRepository.ts b/src/repository/productsRepository.ts index 8b03036..fb8336b 100644 --- a/src/repository/productsRepository.ts +++ b/src/repository/productsRepository.ts @@ -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('products').where({ + user_id, + }).whereIn('product_code', product_codes) } \ No newline at end of file diff --git a/src/repository/restockRepository.ts b/src/repository/restockRepository.ts new file mode 100644 index 0000000..a6b45ec --- /dev/null +++ b/src/repository/restockRepository.ts @@ -0,0 +1,29 @@ +import { db } from "../database/MySQL"; +import { IPurchaseTable } from "../types/db-model"; + +export function insertsDataToRestock(data: Partial[]) { + return db('restocks').insert(data) +} + +export function selectAllRestockHistory( + user_id: IPurchaseTable['user_id'], + limit: number, offset: number +) { + return db('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'); +}; \ No newline at end of file diff --git a/src/repository/suppliersRepository.ts b/src/repository/suppliersRepository.ts index d7cb491..d3c69e9 100644 --- a/src/repository/suppliersRepository.ts +++ b/src/repository/suppliersRepository.ts @@ -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) } diff --git a/src/repository/transactionRepository.ts b/src/repository/transactionRepository.ts new file mode 100644 index 0000000..e567a22 --- /dev/null +++ b/src/repository/transactionRepository.ts @@ -0,0 +1,29 @@ +import { db } from "../database/MySQL"; +import { ITransactionTable } from "../types/db-model"; + +export function insertsDataToTransaction(data: Partial[]) { + return db('transactions').insert(data) +} + +export function selectAllSalesHistory( + user_id: ITransactionTable['user_id'], + limit: number, offset: number +) { + return db('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'); +}; \ No newline at end of file diff --git a/src/routes/api/auth.ts b/src/routes/api/auth.ts index 5721101..925f753 100644 --- a/src/routes/api/auth.ts +++ b/src/routes/api/auth.ts @@ -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 \ No newline at end of file diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts index 36ae95c..4de5e17 100644 --- a/src/routes/api/index.ts +++ b/src/routes/api/index.ts @@ -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; diff --git a/src/routes/api/trx.ts b/src/routes/api/trx.ts new file mode 100644 index 0000000..edefaec --- /dev/null +++ b/src/routes/api/trx.ts @@ -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 \ No newline at end of file diff --git a/src/services/productServices.ts b/src/services/productServices.ts index b23e15b..fcf504c 100644 --- a/src/services/productServices.ts +++ b/src/services/productServices.ts @@ -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() \ No newline at end of file +) => selectProductByProductCode(product_code, user_id).first() + +export const getProductByProductCodes = ( + user_id: IProductTable['user_id'], + product_codes: IProductTable['product_code'][] +) => selectProductByProductCodes(product_codes, user_id) \ No newline at end of file diff --git a/src/services/restockServices.ts b/src/services/restockServices.ts new file mode 100644 index 0000000..0ebd421 --- /dev/null +++ b/src/services/restockServices.ts @@ -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[] = [] + 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 +} \ No newline at end of file diff --git a/src/services/transactionServices.ts b/src/services/transactionServices.ts new file mode 100644 index 0000000..bc80425 --- /dev/null +++ b/src/services/transactionServices.ts @@ -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[] = [] + 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 +} \ No newline at end of file diff --git a/src/types/db-model.ts b/src/types/db-model.ts index 3960fe9..aab4567 100644 --- a/src/types/db-model.ts +++ b/src/types/db-model.ts @@ -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 { diff --git a/src/utils/api/generateCredentialToken.ts b/src/utils/api/generateCredentialToken.ts index e2a4886..ee7f6db 100644 --- a/src/utils/api/generateCredentialToken.ts +++ b/src/utils/api/generateCredentialToken.ts @@ -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' } ); };