final
This commit is contained in:
parent
ecb30ae790
commit
a03c3ab6e8
|
@ -10,11 +10,12 @@
|
|||
"dependencies": {
|
||||
"@ngrok/ngrok": "^1.5.1",
|
||||
"amqplib": "^0.10.3",
|
||||
"axios": "^1.4.0",
|
||||
"axios": "^1.9.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"debug": "~2.6.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
|
@ -35,8 +36,8 @@
|
|||
"node-schedule": "^2.1.1",
|
||||
"nodemailer": "^6.10.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"pg": "^8.11.1",
|
||||
"xlsx": "^0.18.5"
|
||||
"papaparse": "^5.5.3",
|
||||
"pg": "^8.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
|
@ -48,6 +49,7 @@
|
|||
"@types/morgan": "^1.9.9",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
|
@ -575,6 +577,16 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/papaparse": {
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz",
|
||||
"integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
|
@ -695,15 +707,6 @@
|
|||
"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",
|
||||
|
@ -865,9 +868,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
|
@ -1072,19 +1075,6 @@
|
|||
"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",
|
||||
|
@ -1134,15 +1124,6 @@
|
|||
"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",
|
||||
|
@ -1362,18 +1343,6 @@
|
|||
"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",
|
||||
|
@ -1393,6 +1362,12 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -1837,15 +1812,6 @@
|
|||
"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",
|
||||
|
@ -3299,6 +3265,12 @@
|
|||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
|
||||
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
@ -3939,18 +3911,6 @@
|
|||
"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",
|
||||
|
@ -4376,51 +4336,12 @@
|
|||
"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",
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
"dependencies": {
|
||||
"@ngrok/ngrok": "^1.5.1",
|
||||
"amqplib": "^0.10.3",
|
||||
"axios": "^1.4.0",
|
||||
"axios": "^1.9.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"debug": "~2.6.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
|
@ -36,8 +37,8 @@
|
|||
"node-schedule": "^2.1.1",
|
||||
"nodemailer": "^6.10.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"pg": "^8.11.1",
|
||||
"xlsx": "^0.18.5"
|
||||
"papaparse": "^5.5.3",
|
||||
"pg": "^8.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
|
@ -49,6 +50,7 @@
|
|||
"@types/morgan": "^1.9.9",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/papaparse": "^5.3.16",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import { getDashboardData } from "../../services/dashboardServices";
|
||||
import { TAPIResponse } from "../../types/core/http";
|
||||
import createHttpError from "http-errors";
|
||||
import { matchedData, param } from "express-validator";
|
||||
import { selectThisYearSalesTrendMonthly, selectThisYearSalesTrendWeekly } from "../../repository/analytics/sale-trend";
|
||||
import { selectThisYearRestockTrendMonthly, selectThisYearRestockTrendWeekly } from "../../repository/analytics/purchase-trend";
|
||||
import { selectLatestPredictions } from "../../repository/analytics/latest-prediction";
|
||||
|
||||
const dashboardDataController = [
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const totalProduct = await getDashboardData(req.user!.id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: totalProduct
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { cause: error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const dashboardTrendController = [
|
||||
param('period_type')
|
||||
.isIn(['weekly', 'monthly'])
|
||||
.withMessage('period_type must be either "weekly" or "monthly"'),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { period_type } = matchedData(req)
|
||||
|
||||
const sales = await (
|
||||
period_type === 'weekly' ?
|
||||
selectThisYearSalesTrendWeekly() :
|
||||
selectThisYearSalesTrendMonthly()
|
||||
)
|
||||
|
||||
const purchase = await (
|
||||
period_type === 'weekly' ?
|
||||
selectThisYearRestockTrendWeekly() :
|
||||
selectThisYearRestockTrendMonthly()
|
||||
)
|
||||
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: {
|
||||
sales,
|
||||
purchase
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { cause: error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const dashboardLatestPredictionController = [
|
||||
param('period_type')
|
||||
.isIn(['weekly', 'monthly'])
|
||||
.withMessage('period_type must be either "weekly" or "monthly"'),
|
||||
param('trx_type')
|
||||
.isIn(["sales", "purchases"])
|
||||
.withMessage('period_type must be either "sales" or "purchases"'),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const {
|
||||
period_type,
|
||||
trx_type
|
||||
} = matchedData(req)
|
||||
const latest = await selectLatestPredictions(
|
||||
req.user!.id, period_type, trx_type
|
||||
)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: latest
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { cause: error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
dashboardDataController,
|
||||
dashboardTrendController,
|
||||
dashboardLatestPredictionController
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import { body, matchedData, param } from "express-validator"
|
||||
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler"
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
import { insertDummy, selectAllDummyFromProduct, updateDummy } from "../../repository/dummyRepository"
|
||||
import { TAPIResponse } from "../../types/core/http"
|
||||
import createHttpError from "http-errors"
|
||||
import { IDummiesTable } from "../../types/db-model"
|
||||
|
||||
const getSalesDummy = [
|
||||
param('period_type')
|
||||
.isIn(['weekly', 'monthly'])
|
||||
.withMessage('period_type must be either "weekly" or "monthly"'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { period_type } = matchedData<{ period_type: 'weekly' | 'monthly' }>(req)
|
||||
const dummySales = await selectAllDummyFromProduct(req.user!.id, period_type, 'sales')
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: dummySales
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
const getPurchasesDummy = [
|
||||
param('period_type')
|
||||
.isIn(['weekly', 'monthly'])
|
||||
.withMessage('period_type must be either "weekly" or "monthly"'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { period_type } = matchedData<{ period_type: 'weekly' | 'monthly' }>(req)
|
||||
const dummyPurchase = await selectAllDummyFromProduct(req.user!.id, period_type, 'purchases')
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: dummyPurchase
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const createDummy = [
|
||||
body('product_id')
|
||||
.isInt().withMessage('product_id must be an integer'),
|
||||
|
||||
body('fake_json')
|
||||
.custom((val) => {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('fake_json must be a JSON array');
|
||||
}
|
||||
// Optional: Validasi bahwa semua item adalah number
|
||||
if (!parsed.every((item) => typeof item === 'number')) {
|
||||
throw new Error('All items in fake_json must be numbers');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw new Error('fake_json must be a valid JSON array');
|
||||
}
|
||||
}),
|
||||
|
||||
body('period_type')
|
||||
.isIn(['weekly', 'monthly'])
|
||||
.withMessage('period_type must be either "weekly" or "monthly"'),
|
||||
|
||||
body('trx_type')
|
||||
.isIn(['sales', 'purchases'])
|
||||
.withMessage('trx_type must be either "sales" or "purchases"'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const reqData = matchedData<IDummiesTable>(req)
|
||||
const dummy_id = await insertDummy(reqData)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: { dummy_id }
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
const updateDummyController = [
|
||||
param('id'),
|
||||
|
||||
body('fake_json')
|
||||
.custom((val) => {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('fake_json must be a JSON array');
|
||||
}
|
||||
// Optional: Validasi bahwa semua item adalah number
|
||||
if (!parsed.every((item) => typeof item === 'number')) {
|
||||
throw new Error('All items in fake_json must be numbers');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw new Error('fake_json must be a valid JSON array');
|
||||
}
|
||||
}),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = matchedData<IDummiesTable>(req, { locations: ['params'] })
|
||||
const reqData = matchedData<IDummiesTable>(req, { locations: ['body'] })
|
||||
await updateDummy(id, reqData)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Dummy data successfully updated',
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
export default {
|
||||
getPurchasesDummy, getSalesDummy, createDummy, updateDummyController
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import { TAPIResponse } from "../../types/core/http";
|
||||
import createHttpError from "http-errors";
|
||||
import { selectAllProductNPrediction } from "../../repository/predictionRepository";
|
||||
import { body, matchedData, param } from "express-validator";
|
||||
import { makeAutoPrediction } from "../../services/predictionServices";
|
||||
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler";
|
||||
import { IPredictionTable } from "../../types/db-model";
|
||||
import { preparePredictionData } from "../../services/prediction/prepareData";
|
||||
import { getPredictionModel } from "../../services/prediction/getPredictionModel";
|
||||
import { shouldUseAutoModel } from "../../services/prediction/shouldUseAutoModel";
|
||||
import { buildAutoPrediction } from "../../services/prediction/buildAutoPrediction";
|
||||
import { savePredictionModel } from "../../services/prediction/savePredictionModel";
|
||||
import { buildManualPrediction } from "../../services/prediction/buildManualPrediction";
|
||||
import { persistPrediction } from "../../services/prediction/persistPrediction";
|
||||
import { isErrorInstanceOfHttpError } from "../../utils/core/httpError";
|
||||
|
||||
const periodArray = ['daily', 'weekly', 'monthly']
|
||||
|
||||
const filePrediction = [
|
||||
body('csv_string')
|
||||
.notEmpty().withMessage('csv_string tidak boleh kosong.')
|
||||
.isString().withMessage('csv_string harus berupa string.'),
|
||||
|
||||
body('record_period')
|
||||
.notEmpty().withMessage('record_period tidak boleh kosong.')
|
||||
.isIn(['daily', 'weekly', 'monthly']).withMessage('record_period harus bernilai "weekly" atau "monthly".'),
|
||||
|
||||
body('prediction_period')
|
||||
.notEmpty().withMessage('prediction_period tidak boleh kosong.')
|
||||
.isIn(['weekly', 'monthly']).withMessage('prediction_period harus bernilai "weekly" atau "monthly".'),
|
||||
|
||||
body('value_column')
|
||||
.optional()
|
||||
.isString().withMessage('value_column harus berupa string.'),
|
||||
|
||||
body('date_column')
|
||||
.optional()
|
||||
.isString().withMessage('date_column harus berupa string.'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const {
|
||||
csv_string,
|
||||
record_period,
|
||||
prediction_period,
|
||||
value_column,
|
||||
date_column
|
||||
} = matchedData(req)
|
||||
const rp_index = periodArray.findIndex(v => v === record_period)
|
||||
const pp_index = periodArray.findIndex(v => v === prediction_period)
|
||||
if (pp_index < 0 || rp_index < 0) {
|
||||
next(createHttpError(400, 'record_period atau prediction_period tidak valid.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pp_index < rp_index) {
|
||||
next(createHttpError(400, 'prediction_period tidak boleh memiliki resolusi lebih kecil dari record_period.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pythonResponse = await makeAutoPrediction({
|
||||
csv_string,
|
||||
date_column,
|
||||
value_column,
|
||||
prediction_period,
|
||||
date_regroup: record_period !== prediction_period,
|
||||
})
|
||||
|
||||
if (!pythonResponse?.mape || pythonResponse.mape > 50) {
|
||||
throw createHttpError(422, `Prediksi tidak layak karena MAPE terlalu tinggi (${pythonResponse?.mape.toFixed(2) || NaN}%). Harap periksa data input.`)
|
||||
}
|
||||
|
||||
if (pythonResponse) {
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Prediksi berhasil dilakukan.',
|
||||
data: pythonResponse,
|
||||
};
|
||||
res.json(result);
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Gagal melakukan prediksi. Silakan coba lagi atau cek data input.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const getSavedPurchasePredictions = [
|
||||
param('period_type')
|
||||
.isIn(['weekly', 'monthly'])
|
||||
.withMessage('period_type must be either "weekly" or "monthly"'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { period_type } = matchedData<{ period_type: 'weekly' | 'monthly' }>(req)
|
||||
const productNPrediction = await selectAllProductNPrediction(req.user!.id, period_type, 'purchases')
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: productNPrediction
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const getSavedSalePredictions = [
|
||||
param('period_type')
|
||||
.isIn(['weekly', 'monthly'])
|
||||
.withMessage('period_type must be either "weekly" or "monthly"'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { period_type } = matchedData<{ period_type: 'weekly' | 'monthly' }>(req)
|
||||
const productNPrediction = await selectAllProductNPrediction(req.user!.id, period_type, 'sales')
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: productNPrediction
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(createHttpError(500, "Internal Server Error", { error }));
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const purchasePrediction = [
|
||||
param('product_id').exists().withMessage('product_id harus disediakan.')
|
||||
.isInt({ gt: 0 }).withMessage('product_id harus berupa angka bulat positif.')
|
||||
.toInt(),
|
||||
|
||||
body('prediction_period')
|
||||
.notEmpty().withMessage('prediction_period tidak boleh kosong.')
|
||||
.isIn(['weekly', 'monthly']).withMessage('prediction_period harus bernilai "weekly" atau "monthly".'),
|
||||
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const source: IPredictionTable['prediction_source'] = 'purchases'
|
||||
const { product_id, prediction_period } = matchedData(req)
|
||||
const csv_string = await preparePredictionData(product_id, prediction_period, source)
|
||||
const { model, isExpired } = await getPredictionModel(product_id, prediction_period, source)
|
||||
|
||||
let predictionResult
|
||||
if (shouldUseAutoModel(model, isExpired)) {
|
||||
predictionResult = await buildAutoPrediction(csv_string, prediction_period)
|
||||
await savePredictionModel(model, predictionResult, product_id, prediction_period, source, req.user!.id)
|
||||
} else {
|
||||
const arimaModel: [number, number, number] = [model!.ar_p, model!.differencing_d, model!.ma_q]
|
||||
predictionResult = await buildManualPrediction(csv_string, prediction_period, arimaModel)
|
||||
}
|
||||
|
||||
const finalResult = await persistPrediction(predictionResult, prediction_period, source, product_id, req.user!.id)
|
||||
|
||||
res.status(200).json({ success: true, data: finalResult })
|
||||
} catch (err) {
|
||||
if (isErrorInstanceOfHttpError(err))
|
||||
return next(err)
|
||||
return next(createHttpError(500, 'Internal Server Error', { cause: err }))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const salesPrediction = [
|
||||
param('product_id').exists().withMessage('product_id harus disediakan.')
|
||||
.isInt({ gt: 0 }).withMessage('product_id harus berupa angka bulat positif.')
|
||||
.toInt(),
|
||||
|
||||
body('prediction_period')
|
||||
.notEmpty().withMessage('prediction_period tidak boleh kosong.')
|
||||
.isIn(['weekly', 'monthly']).withMessage('prediction_period harus bernilai "weekly" atau "monthly".'),
|
||||
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const source: IPredictionTable['prediction_source'] = 'sales'
|
||||
const { product_id, prediction_period } = matchedData(req)
|
||||
const csv_string = await preparePredictionData(product_id, prediction_period, source)
|
||||
const { model, isExpired } = await getPredictionModel(product_id, prediction_period, source)
|
||||
|
||||
let predictionResult
|
||||
if (shouldUseAutoModel(model, isExpired)) {
|
||||
predictionResult = await buildAutoPrediction(csv_string, prediction_period)
|
||||
await savePredictionModel(model, predictionResult, product_id, prediction_period, source, req.user!.id)
|
||||
} else {
|
||||
const arimaModel: [number, number, number] = [model!.ar_p, model!.differencing_d, model!.ma_q]
|
||||
predictionResult = await buildManualPrediction(csv_string, prediction_period, arimaModel)
|
||||
}
|
||||
|
||||
const finalResult = await persistPrediction(predictionResult, prediction_period, source, product_id, req.user!.id)
|
||||
|
||||
res.status(200).json({ success: true, data: finalResult })
|
||||
} catch (err) {
|
||||
if (isErrorInstanceOfHttpError(err))
|
||||
return next(err)
|
||||
return next(createHttpError(500, 'Internal Server Error', { cause: err }))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
filePrediction,
|
||||
getSavedPurchasePredictions,
|
||||
getSavedSalePredictions,
|
||||
salesPrediction,
|
||||
purchasePrediction
|
||||
}
|
|
@ -65,34 +65,6 @@ const deleteProductCategoryRoute = [
|
|||
}
|
||||
]
|
||||
|
||||
const updateProductCategory = [
|
||||
param("id").notEmpty(),
|
||||
body("category_name").notEmpty(),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const reqParam = matchedData(req, { locations: ['params'] })
|
||||
const reqBody = matchedData<IProductCategoryTable>(req, { locations: ['body'] })
|
||||
try {
|
||||
const newProductId = await updateProductCategoryById(reqParam.id, {
|
||||
...reqBody,
|
||||
category_name: reqBody.category_name.toLowerCase()
|
||||
})
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Product category successfully updated.',
|
||||
data: {
|
||||
productCategoryId: newProductId
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error: unknown) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
export default {
|
||||
addProductCategory, getProductCategories, deleteProductCategoryRoute
|
||||
}
|
|
@ -5,7 +5,8 @@ import { TAPIResponse } from "../../types/core/http"
|
|||
import { IProductTable, ISupplierTable } from "../../types/db-model"
|
||||
import createHttpError from "http-errors"
|
||||
import parsePhoneNumberFromString from "libphonenumber-js"
|
||||
import { createProduct, deleteProductById, getProductByProductCode, getProducts, showProductById, updateProductById } from "../../services/productServices"
|
||||
import { createProduct, deleteProductById, getLowStockProducts, getProductByProductCode, getProducts, showProductById, updateProductById } from "../../services/productServices"
|
||||
import { selectShortDataAllProduct } from "../../repository/productsRepository"
|
||||
|
||||
const addProduct = [
|
||||
body('product_code').notEmpty().isString(),
|
||||
|
@ -163,4 +164,39 @@ const getProductByProductCodeRoute = [
|
|||
}
|
||||
]
|
||||
|
||||
export default { addProduct, showAllProducts, getProductDetail, updateProductRoute, deleteProductRoute, getProductByProductCodeRoute }
|
||||
const getLowStockProduct = [
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const product = await getLowStockProducts(req.user!.id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: product
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const getAllProduct = [
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const product = await selectShortDataAllProduct(req.user!.id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: product
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
addProduct, showAllProducts,
|
||||
getProductDetail, updateProductRoute,
|
||||
deleteProductRoute, getProductByProductCodeRoute,
|
||||
getLowStockProduct, getAllProduct
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
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 { 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,
|
||||
// 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) => {
|
||||
res.json({
|
||||
status: true,
|
||||
message: 'File Saved'
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
// createStockForecasting,
|
||||
// getStockForecastingByDate,
|
||||
// getStockForecastings
|
||||
makePredictionFromSheet
|
||||
};
|
|
@ -1,103 +0,0 @@
|
|||
// controllers/userFilesController.ts
|
||||
|
||||
import { body, param, matchedData } from "express-validator";
|
||||
import createHttpError from "http-errors";
|
||||
import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware
|
||||
import * as userFilesRepository from "../../repository/user-files"; // Repository untuk User Files
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { IUserFile } from "../../types/db-model";
|
||||
|
||||
// 🟢 **POST: Tambah User File Baru**
|
||||
const createUserFile = [
|
||||
body("file_name").notEmpty().withMessage("File name is required"),
|
||||
body("saved_file_name").notEmpty().withMessage("Saved file name is required"),
|
||||
body("is_converted").optional().isBoolean().withMessage("is_converted must be a boolean"),
|
||||
body("user_id").notEmpty().isInt().withMessage("Invalid user ID"), // ganti isMongoId() ke isInt()
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = matchedData<IUserFile>(req);
|
||||
const userFile = await userFilesRepository.createUserFile(data);
|
||||
|
||||
res.status(201).json({ message: "User file created", userFile });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🟠 **PATCH: Perbarui File User**
|
||||
const updateUserFile = [
|
||||
param("id").isInt().withMessage("Invalid file ID"), // ganti isMongoId() ke isInt()
|
||||
body("file_name").optional().isString(),
|
||||
body("saved_file_name").optional().isString(),
|
||||
body("is_converted").optional().isBoolean(),
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = matchedData<IUserFile>(req);
|
||||
|
||||
const userFile = await userFilesRepository.updateUserFileById(Number(id), data);
|
||||
if (!userFile) return next(createHttpError(404, "User file not found"));
|
||||
|
||||
res.json({ message: "User file updated", userFile });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🔴 **DELETE: Hapus File User**
|
||||
const deleteUserFile = [
|
||||
param("id").isInt().withMessage("Invalid file ID"), // ganti isMongoId() ke isInt()
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const userFile = await userFilesRepository.deleteUserFileById(Number(id));
|
||||
if (!userFile) return next(createHttpError(404, "User file not found"));
|
||||
|
||||
res.json({ message: "User file deleted" });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🔵 **GET: Ambil Satu File User Berdasarkan ID**
|
||||
const getUserFile = [
|
||||
param("id").isInt().withMessage("Invalid file ID"), // ganti isMongoId() ke isInt()
|
||||
validate,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userFile = await userFilesRepository.getUserFileById(Number(id));
|
||||
|
||||
if (!userFile) return next(createHttpError(404, "User file not found"));
|
||||
|
||||
res.json(userFile);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 🟡 **GET: Ambil Semua File User**
|
||||
const getUserFiles = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const allUserFiles = await userFilesRepository.getAllUserFiles();
|
||||
res.json(allUserFiles);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createUserFile,
|
||||
updateUserFile,
|
||||
deleteUserFile,
|
||||
getUserFile,
|
||||
getUserFiles
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { db } from "../../database/MySQL"
|
||||
import { IPredictionTable } from "../../types/db-model"
|
||||
|
||||
export function selectLatestPredictions(
|
||||
user_id: IPredictionTable['user_id'],
|
||||
periodType: IPredictionTable['period_type'],
|
||||
predictionSource: IPredictionTable['prediction_source']
|
||||
) {
|
||||
return db('predictions as p')
|
||||
.join('products as pr', 'p.product_id', 'pr.id')
|
||||
.join('product_categories as pc', 'pr.product_category_id', 'pc.id')
|
||||
.select(
|
||||
'pr.product_name',
|
||||
'p.mape',
|
||||
'p.prediction',
|
||||
'pc.category_name',
|
||||
)
|
||||
.where({
|
||||
'p.prediction_source': predictionSource,
|
||||
'p.period_type': periodType,
|
||||
'p.user_id': user_id
|
||||
})
|
||||
.andWhere('p.expired', '>', db.fn.now())
|
||||
.orderBy('p.expired', 'desc')
|
||||
.limit(4);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { db } from "../../database/MySQL";
|
||||
|
||||
export function selectThisYearRestockTrendMonthly() {
|
||||
return db('restocks')
|
||||
.select(
|
||||
db.raw('YEAR(buying_date) AS year'),
|
||||
db.raw('MONTH(buying_date) AS month'),
|
||||
db.raw('SUM(amount) AS amount')
|
||||
)
|
||||
.whereRaw('YEAR(buying_date) = YEAR(CURDATE())')
|
||||
.groupByRaw('YEAR(buying_date), MONTH(buying_date)')
|
||||
.orderByRaw('YEAR(buying_date), MONTH(buying_date)')
|
||||
}
|
||||
|
||||
export function selectThisYearRestockTrendWeekly() {
|
||||
return db('restocks')
|
||||
.select(
|
||||
db.raw('YEAR(buying_date) AS year'),
|
||||
db.raw('WEEK(buying_date, 1) AS week'),
|
||||
db.raw('SUM(amount) AS amount')
|
||||
)
|
||||
.whereRaw('YEAR(buying_date) = YEAR(CURDATE())')
|
||||
.groupByRaw('YEAR(buying_date), WEEK(buying_date, 1)')
|
||||
.orderByRaw('YEAR(buying_date), WEEK(buying_date, 1)')
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { db } from "../../database/MySQL";
|
||||
|
||||
export function selectThisYearSalesTrendMonthly() {
|
||||
return db('transactions')
|
||||
.select(
|
||||
db.raw("YEAR(transaction_date) AS year"),
|
||||
db.raw("MONTH(transaction_date) AS month"),
|
||||
db.raw("SUM(amount) AS amount")
|
||||
)
|
||||
.whereRaw("YEAR(transaction_date) = YEAR(CURDATE())") // filter tahun ini
|
||||
.groupByRaw("YEAR(transaction_date), MONTH(transaction_date)")
|
||||
.orderByRaw("YEAR(transaction_date), MONTH(transaction_date)")
|
||||
}
|
||||
|
||||
export function selectThisYearSalesTrendWeekly() {
|
||||
return db('transactions')
|
||||
.select(
|
||||
db.raw("YEAR(transaction_date) AS year"),
|
||||
db.raw("WEEK(transaction_date, 1) AS week"), // mode 1: ISO week (Senin awal minggu)
|
||||
db.raw("SUM(amount) AS amount")
|
||||
)
|
||||
.whereRaw("YEAR(transaction_date) = YEAR(CURDATE())")
|
||||
.groupByRaw("YEAR(transaction_date), WEEK(transaction_date, 1)")
|
||||
.orderByRaw("YEAR(transaction_date), WEEK(transaction_date, 1)")
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { IDummiesTable, IProductTable } from "../types/db-model";
|
||||
|
||||
export function selectAllDummyFromProduct(
|
||||
user_id: IProductTable['user_id'],
|
||||
period_type: IDummiesTable['period_type'],
|
||||
trx_type: IDummiesTable['trx_type']
|
||||
) {
|
||||
return db('products')
|
||||
.leftJoin('dummies', function () {
|
||||
this.on('dummies.product_id', '=', 'products.id')
|
||||
.andOn(db.raw('dummies.period_type = ?', [period_type]))
|
||||
.andOn(db.raw('dummies.trx_type = ?', [trx_type]))
|
||||
})
|
||||
.where('products.user_id', user_id)
|
||||
.andWhere('products.deleted', false)
|
||||
.select(
|
||||
'products.id as product_id',
|
||||
'products.product_name',
|
||||
'dummies.id as dummy_id',
|
||||
'dummies.fake_json'
|
||||
)
|
||||
}
|
||||
|
||||
export async function insertDummy(data: IDummiesTable) {
|
||||
const [id] = await db<IDummiesTable>('dummies').insert(data)
|
||||
return id
|
||||
}
|
||||
|
||||
export function updateDummy(id: IDummiesTable['id'], data: IDummiesTable) {
|
||||
return db<IDummiesTable>('dummies').where({ id }).update(data)
|
||||
}
|
||||
|
||||
export function selectDummy(product_id: IDummiesTable['product_id'], period_type: IDummiesTable['period_type'], trx_type: IDummiesTable['trx_type']) {
|
||||
return db<IDummiesTable>('dummies')
|
||||
.where({ product_id, period_type, trx_type })
|
||||
.first()
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { IPredictionModelTable } from "../types/db-model";
|
||||
|
||||
export function selectPredictionModel(
|
||||
product_id: IPredictionModelTable['product_id'],
|
||||
period_type: IPredictionModelTable['period_type'],
|
||||
prediction_source: IPredictionModelTable['prediction_source']
|
||||
) {
|
||||
return db<IPredictionModelTable>('prediction_models')
|
||||
.where({ product_id, period_type, prediction_source })
|
||||
.first()
|
||||
}
|
||||
|
||||
export async function insertPredictionModel(
|
||||
data: Partial<IPredictionModelTable>
|
||||
) {
|
||||
const [id] = await db<IPredictionModelTable>('prediction_models').insert(data)
|
||||
return id
|
||||
}
|
||||
|
||||
export async function updatePredictionModel(
|
||||
id: IPredictionModelTable['id'],
|
||||
data: Partial<IPredictionModelTable>
|
||||
) {
|
||||
return await db<IPredictionModelTable>('prediction_models').where({ id }).update(data)
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { IPredictionTable, IProductTable, ITransactionTable } from "../types/db-model";
|
||||
import { TGroupedProductSales } from "../types/db/product-sales";
|
||||
import { selectProductsByUserId } from "./productsRepository";
|
||||
|
||||
export async function selectWeeklyProductSales(
|
||||
product_id: ITransactionTable['product_id']
|
||||
) {
|
||||
return db.raw<TGroupedProductSales[]>(`
|
||||
SELECT
|
||||
DATE_SUB(DATE(t.transaction_date), INTERVAL WEEKDAY(t.transaction_date) DAY) AS group_date,
|
||||
COUNT(*) AS total_transactions
|
||||
FROM transactions t
|
||||
WHERE t.product_id = ?
|
||||
GROUP BY group_date
|
||||
ORDER BY group_date ASC
|
||||
`, [product_id]);
|
||||
}
|
||||
|
||||
export async function selectMonthlyProductSales(
|
||||
product_id: ITransactionTable['product_id']
|
||||
) {
|
||||
return db.raw<TGroupedProductSales[]>(`
|
||||
SELECT
|
||||
DATE_FORMAT(t.created_at, '%Y-%m') AS group_date,
|
||||
p.product_name,
|
||||
p.product_code,
|
||||
t.product_id,
|
||||
COUNT(*) AS total_transactions
|
||||
FROM transactions t
|
||||
LEFT JOIN products p ON t.product_id = p.id
|
||||
WHERE t.product_id = ?
|
||||
GROUP BY group_date, p.product_name, p.product_code, t.product_id
|
||||
ORDER BY group_date
|
||||
`, [product_id])
|
||||
}
|
||||
|
||||
export async function selectAllProductNPrediction(
|
||||
user_id: IProductTable['user_id'],
|
||||
period_type: IPredictionTable['period_type'],
|
||||
prediction_source: IPredictionTable['prediction_source']
|
||||
) {
|
||||
return db('products')
|
||||
.leftJoin('predictions', function () {
|
||||
this.on('predictions.product_id', '=', 'products.id')
|
||||
.andOn(db.raw('predictions.period_type = ?', [period_type]))
|
||||
.andOn(db.raw('predictions.prediction_source = ?', [prediction_source]))
|
||||
.andOn(db.raw('predictions.expired >= ?', [new Date().toISOString()]));
|
||||
})
|
||||
// .leftJoin('dummies', function () {
|
||||
// this.on('dummies.product_id', '=', 'products.id')
|
||||
// .andOn(db.raw('dummies.period_type = ?', [period_type]))
|
||||
// .andOn(db.raw('dummies.trx_type = ?', [prediction_source]))
|
||||
// })
|
||||
.where('products.user_id', user_id)
|
||||
.andWhere('products.deleted', false)
|
||||
.select(
|
||||
'products.id',
|
||||
'products.product_name',
|
||||
'products.buying_price',
|
||||
'products.stock',
|
||||
'products.low_stock_limit',
|
||||
'predictions.prediction',
|
||||
'predictions.lower_bound',
|
||||
'predictions.upper_bound',
|
||||
'predictions.rmse',
|
||||
'predictions.mape',
|
||||
// 'dummies.fake_json',
|
||||
)
|
||||
}
|
||||
|
||||
export function selectPrediction(
|
||||
product_id: IPredictionTable['product_id'],
|
||||
period_type: IPredictionTable['period_type'],
|
||||
prediction_source: IPredictionTable['prediction_source']
|
||||
) {
|
||||
return db<IPredictionTable>('predictions')
|
||||
.where({ product_id, period_type, prediction_source })
|
||||
.first()
|
||||
}
|
||||
|
||||
export async function insertPrediction(data: Partial<IPredictionTable>) {
|
||||
const [id] = await db<IPredictionTable>('predictions').insert(data)
|
||||
return id
|
||||
}
|
||||
|
||||
export function updatePrediction(id: IPredictionTable['id'], data: Partial<IPredictionTable>) {
|
||||
return db<IPredictionTable>('predictions').where({ id }).update(data)
|
||||
}
|
|
@ -7,7 +7,7 @@ export const insertProduct = async (data: IProductTable) => {
|
|||
}
|
||||
|
||||
export const selectProductsByUserId = (user_id: IProductTable['user_id']) => {
|
||||
return db<IProductTable>('products').where({ user_id })
|
||||
return db<IProductTable>('products').where('products.user_id', user_id)
|
||||
}
|
||||
|
||||
export const selectProductById = (id: IProductTable['id']) => {
|
||||
|
@ -61,4 +61,34 @@ export const selectProductByProductCodes = (product_codes: IProductTable['produc
|
|||
return db<IProductTable>('products').where({
|
||||
user_id,
|
||||
}).whereIn('product_code', product_codes)
|
||||
}
|
||||
|
||||
export function selectLowStockProducts(user_id: IProductTable['user_id']) {
|
||||
return db<IProductTable>('products as p')
|
||||
.leftJoin('product_categories as pc', 'pc.id', 'p.product_category_id')
|
||||
.select('p.id', 'p.product_name', 'p.stock', 'p.low_stock_limit', 'p.buying_price', 'pc.category_name')
|
||||
.whereRaw('p.stock < p.low_stock_limit')
|
||||
.andWhere('p.deleted', 0)
|
||||
.andWhere('p.user_id', user_id)
|
||||
}
|
||||
|
||||
export function selectLowStockProductCount(user_id: IProductTable['user_id']) {
|
||||
return db('products')
|
||||
.where({ user_id: user_id, deleted: false })
|
||||
.whereRaw('stock < low_stock_limit')
|
||||
.count('id as total')
|
||||
.first<{
|
||||
total: number
|
||||
}>();
|
||||
}
|
||||
|
||||
export function selectShortDataAllProduct(
|
||||
user_id: IProductTable['user_id']
|
||||
) {
|
||||
return db<IProductTable>('products')
|
||||
.where({ user_id, deleted: false })
|
||||
.select(
|
||||
'product_code',
|
||||
'product_name'
|
||||
)
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { IPurchaseTable } from "../types/db-model";
|
||||
import { getStartOfMonth, getStartOfWeek } from "../utils/core/date";
|
||||
|
||||
export function insertsDataToRestock(data: Partial<IPurchaseTable>[]) {
|
||||
return db<IPurchaseTable>('restocks').insert(data)
|
||||
|
@ -26,4 +27,50 @@ export const countRestocks = async (id_user: number) => {
|
|||
}>();
|
||||
|
||||
return parseInt(result?.count || '0');
|
||||
};
|
||||
};
|
||||
|
||||
export const selectMonthlyRestockCount = (user_id: number) => {
|
||||
return db('restocks')
|
||||
.whereRaw('MONTH(buying_date) = MONTH(CURRENT_DATE()) AND YEAR(buying_date) = YEAR(CURRENT_DATE())')
|
||||
.andWhere('user_id', user_id)
|
||||
.sum('total_price as total')
|
||||
.first<{
|
||||
total: number
|
||||
}>();
|
||||
}
|
||||
|
||||
export const selectLastMonthRestockCount = (user_id: number) => {
|
||||
return db('restocks')
|
||||
.whereRaw('MONTH(buying_date) = MONTH(CURRENT_DATE() - INTERVAL 1 MONTH) AND YEAR(buying_date) = YEAR(CURRENT_DATE() - INTERVAL 1 MONTH)')
|
||||
.andWhere('user_id', user_id)
|
||||
.sum('total_price as total')
|
||||
.first<{
|
||||
total: number
|
||||
}>();
|
||||
}
|
||||
|
||||
export function selectGroupedWeeklyPurchase(productId: IPurchaseTable['product_id']) {
|
||||
return db<IPurchaseTable>('restocks')
|
||||
.select(
|
||||
db.raw("YEAR(buying_date) as year"),
|
||||
db.raw("WEEK(buying_date, 1) as week"),
|
||||
db.raw("SUM(amount) as amount")
|
||||
)
|
||||
.where('product_id', productId)
|
||||
.andWhere('buying_date', '<', getStartOfWeek(new Date()).toISOString())
|
||||
.groupBy(['year', 'week'])
|
||||
.orderBy(['year', 'week'])
|
||||
}
|
||||
|
||||
export function selectGroupedMonthlyPurchase(productId: IPurchaseTable['product_id']) {
|
||||
return db<IPurchaseTable>('restocks')
|
||||
.select(
|
||||
db.raw("YEAR(buying_date) as year"),
|
||||
db.raw("MONTH(buying_date) as month"),
|
||||
db.raw("SUM(amount) as amount")
|
||||
)
|
||||
.where('product_id', productId)
|
||||
.andWhere('buying_date', '<', getStartOfMonth(new Date()).toISOString())
|
||||
.groupBy(['year', 'month'])
|
||||
.orderBy(['year', 'month'])
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
// repository/transactionsRepository.ts
|
||||
|
||||
import { db } from '../database/MySQL';
|
||||
import { ITransaction } from '../types/db-model';
|
||||
|
||||
export const createTransaction = async (data: ITransaction) => {
|
||||
try {
|
||||
const [transaction] = await db('transactions').insert(data).returning('*');
|
||||
return transaction;
|
||||
} catch (err) {
|
||||
throw new Error('Error creating transaction');
|
||||
}
|
||||
};
|
||||
|
||||
export const getTransactionById = async (id: number) => {
|
||||
try {
|
||||
const transaction = await db('transactions')
|
||||
.where({ id })
|
||||
.join('products', 'transactions.product_id', '=', 'products.id')
|
||||
.join('users', 'transactions.user_id', '=', 'users.id')
|
||||
.select('transactions.*', 'products.product_name', 'users.username');
|
||||
return transaction[0];
|
||||
} catch (err) {
|
||||
throw new Error('Error fetching transaction');
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTransactionById = async (id: number, data: ITransaction) => {
|
||||
try {
|
||||
const [updatedTransaction] = await db('transactions').where({ id }).update(data).returning('*');
|
||||
return updatedTransaction;
|
||||
} catch (err) {
|
||||
throw new Error('Error updating transaction');
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTransactionById = async (id: number) => {
|
||||
try {
|
||||
const transaction = await db('transactions').where({ id }).del();
|
||||
return transaction;
|
||||
} catch (err) {
|
||||
throw new Error('Error deleting transaction');
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllTransactions = async () => {
|
||||
try {
|
||||
return await db('transactions')
|
||||
.join('products', 'transactions.product_id', '=', 'products.id')
|
||||
.join('users', 'transactions.user_id', '=', 'users.id')
|
||||
.select('transactions.*', 'products.product_name', 'users.username');
|
||||
} catch (err) {
|
||||
throw new Error('Error fetching all transactions');
|
||||
}
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { ITransactionTable } from "../types/db-model";
|
||||
import { getStartOfMonth, getStartOfWeek } from "../utils/core/date";
|
||||
|
||||
export function insertsDataToTransaction(data: Partial<ITransactionTable>[]) {
|
||||
return db<ITransactionTable>('transactions').insert(data)
|
||||
|
@ -26,4 +27,52 @@ export const countSales = async (id_user: number) => {
|
|||
}>();
|
||||
|
||||
return parseInt(result?.count || '0');
|
||||
};
|
||||
};
|
||||
|
||||
export const selectLastMonthSales = async (user_id: number) => {
|
||||
return db('transactions')
|
||||
.whereRaw('MONTH(transaction_date) = MONTH(CURRENT_DATE() - INTERVAL 1 MONTH) AND YEAR(transaction_date) = YEAR(CURRENT_DATE() - INTERVAL 1 MONTH)')
|
||||
.andWhere('user_id', user_id)
|
||||
.sum('total_price as total')
|
||||
.first<{
|
||||
total: number
|
||||
}>();
|
||||
}
|
||||
|
||||
export const selectCurrentMonthSales = async (user_id: number) => {
|
||||
return db('transactions')
|
||||
.whereRaw('MONTH(transaction_date) = MONTH(CURRENT_DATE()) AND YEAR(transaction_date) = YEAR(CURRENT_DATE())')
|
||||
.andWhere('user_id', user_id)
|
||||
.sum('total_price as total')
|
||||
.first<{
|
||||
total: number
|
||||
}>();
|
||||
}
|
||||
|
||||
export function selectGroupedWeeklySales(productId: ITransactionTable['product_id']) {
|
||||
return db('transactions')
|
||||
.select(
|
||||
db.raw("YEAR(transaction_date) as year"),
|
||||
db.raw("WEEK(transaction_date, 1) as week"),
|
||||
db.raw("SUM(amount) as amount")
|
||||
)
|
||||
.where('product_id', productId)
|
||||
.andWhere('transaction_date', '<', getStartOfWeek(new Date()).toISOString())
|
||||
|
||||
.groupBy(['year', 'week'])
|
||||
.orderBy(['year', 'week'])
|
||||
}
|
||||
|
||||
export function selectGroupedMonthlySales(productId: ITransactionTable['product_id']) {
|
||||
return db('transactions')
|
||||
.select(
|
||||
db.raw("YEAR(transaction_date) as year"),
|
||||
db.raw("MONTH(transaction_date) as month"),
|
||||
db.raw("SUM(amount) as amount")
|
||||
)
|
||||
.where('product_id', productId)
|
||||
.andWhere('transaction_date', '<', getStartOfMonth(new Date()).toISOString())
|
||||
|
||||
.groupBy(['year', 'month'])
|
||||
.orderBy(['year', 'month'])
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import express from 'express'
|
||||
var router = express.Router();
|
||||
import authRoute from '../../controller/api/authController';
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.post('/register', authRoute.register);
|
||||
router.post('/login', authRoute.login);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import express from 'express'
|
||||
var router = express.Router();
|
||||
import dashboardController from '../../controller/api/dashboardController'
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.get('/dashboard', authenticate, dashboardController.dashboardDataController)
|
||||
router.get('/dashboard/trend/:period_type', authenticate, dashboardController.dashboardTrendController)
|
||||
router.get('/dashboard/latest/:trx_type/:period_type', authenticate, dashboardController.dashboardLatestPredictionController)
|
||||
|
||||
export default router
|
|
@ -0,0 +1,11 @@
|
|||
import express from 'express'
|
||||
var router = express.Router();
|
||||
import dummyController from '../../controller/api/dummyController'
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.get('/dummies/purchases/:period_type', authenticate, dummyController.getPurchasesDummy)
|
||||
router.get('/dummies/sales/:period_type', authenticate, dummyController.getSalesDummy)
|
||||
router.post('/dummy', authenticate, dummyController.createDummy)
|
||||
router.patch('/dummy/:id', authenticate, dummyController.updateDummyController)
|
||||
|
||||
export default router
|
|
@ -5,7 +5,10 @@ import authEndpoint from './auth'
|
|||
import supplierEndpoint from './supplier'
|
||||
import productCategoryEndpoint from './product_category'
|
||||
import trxEndpoint from './trx'
|
||||
import dashboardEndpoint from './dashboard'
|
||||
import predictionEndpoint from './prediction'
|
||||
import productEndpoint from './product'
|
||||
import dummyEndpoint from './dummy'
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
|
@ -25,5 +28,8 @@ router.use('/', supplierEndpoint)
|
|||
router.use('/', productEndpoint)
|
||||
router.use('/', productCategoryEndpoint)
|
||||
router.use('/', trxEndpoint)
|
||||
router.use('/', dashboardEndpoint)
|
||||
router.use('/', predictionEndpoint)
|
||||
router.use('/', dummyEndpoint)
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import express from 'express'
|
||||
var router = express.Router();
|
||||
import predictionController from '../../controller/api/predictionController'
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.post('/prediction-from-file', predictionController.filePrediction)
|
||||
router.get('/saved-predictions/purchases/:period_type', authenticate, predictionController.getSavedPurchasePredictions)
|
||||
router.get('/saved-predictions/sales/:period_type', authenticate, predictionController.getSavedSalePredictions)
|
||||
router.post('/purchase-prediction/:product_id', authenticate, predictionController.purchasePrediction)
|
||||
router.post('/sales-prediction/:product_id', authenticate, predictionController.salesPrediction)
|
||||
export default router
|
|
@ -3,12 +3,13 @@ var router = express.Router();
|
|||
import productController from '../../controller/api/productsController';
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.use('/', authenticate)
|
||||
router.post('/product', productController.addProduct);
|
||||
router.get('/product/:product_code', productController.getProductByProductCodeRoute);
|
||||
router.get('/products', productController.showAllProducts);
|
||||
router.get('/product/:id', productController.getProductDetail);
|
||||
router.patch('/product/:id', productController.updateProductRoute);
|
||||
router.delete('/product/:id', productController.deleteProductRoute);
|
||||
router.post('/product', authenticate, productController.addProduct);
|
||||
router.get('/product/:product_code', authenticate, productController.getProductByProductCodeRoute);
|
||||
router.get('/products', authenticate, productController.showAllProducts);
|
||||
router.get('/all-products/limited', authenticate, productController.getAllProduct);
|
||||
router.get('/product-get-by-id/:id', authenticate, productController.getProductDetail);
|
||||
router.patch('/product/:id', authenticate, productController.updateProductRoute);
|
||||
router.delete('/product/:id', authenticate, productController.deleteProductRoute);
|
||||
router.get('/product-low-stock', authenticate, productController.getLowStockProduct)
|
||||
|
||||
export default router
|
|
@ -3,10 +3,9 @@ var router = express.Router();
|
|||
import productController from '../../controller/api/productCategoriesController';
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
|
||||
router.use('/', authenticate)
|
||||
router.post('/product-category', productController.addProductCategory);
|
||||
router.post('/product-category/:id', productController.addProductCategory);
|
||||
router.get('/product-categories', productController.getProductCategories);
|
||||
router.delete('/product-category/:id', productController.deleteProductCategoryRoute);
|
||||
router.post('/product-category', authenticate, productController.addProductCategory);
|
||||
router.post('/product-category/:id', authenticate, productController.addProductCategory);
|
||||
router.get('/product-categories', authenticate, productController.getProductCategories);
|
||||
router.delete('/product-category/:id', authenticate, productController.deleteProductCategoryRoute);
|
||||
|
||||
export default router
|
|
@ -3,11 +3,10 @@ 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);
|
||||
router.post('/supplier', authenticate, supplierController.addSupplier);
|
||||
router.get('/suppliers', authenticate, supplierController.showAllSupplier);
|
||||
router.get('/supplier/:id', authenticate, supplierController.getSupplierDetail);
|
||||
router.patch('/supplier/:id', authenticate, supplierController.updateSupplierRoute);
|
||||
router.delete('/supplier/:id', authenticate, supplierController.deleteSupplierRoute);
|
||||
|
||||
export default router
|
|
@ -3,10 +3,9 @@ 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);
|
||||
router.post('/restocks', authenticate, transactionController.createRestockRecord);
|
||||
router.post('/transactions', authenticate, transactionController.createTransactionRecord);
|
||||
router.get('/restocks-history', authenticate, transactionController.showRestockHistoryRoute);
|
||||
router.get('/sales-history', authenticate, transactionController.showSalesHistoryRoute);
|
||||
|
||||
export default router
|
|
@ -0,0 +1,23 @@
|
|||
import { countProducts, selectLowStockProductCount } from "../repository/productsRepository"
|
||||
import { selectLastMonthRestockCount, selectMonthlyRestockCount } from "../repository/restockRepository"
|
||||
import { selectLastMonthSales, selectCurrentMonthSales } from "../repository/transactionRepository"
|
||||
import { calculateGrowth } from '../utils/math/calculateGrowth'
|
||||
|
||||
export async function getDashboardData(user_id: number) {
|
||||
const total_product = await countProducts(user_id)
|
||||
const { total: total_low_stock } = await selectLowStockProductCount(user_id)
|
||||
const { total: total_last_month_sales } = await selectLastMonthSales(user_id)
|
||||
const { total: total_monthly_sales } = await selectCurrentMonthSales(user_id)
|
||||
const { total: total_last_month_restock } = await selectLastMonthRestockCount(user_id)
|
||||
const { total: total_monthly_restock } = await selectMonthlyRestockCount(user_id)
|
||||
return {
|
||||
total_product,
|
||||
total_low_stock,
|
||||
total_last_month_restock,
|
||||
total_monthly_restock,
|
||||
restock_growth: Number(calculateGrowth(Number(selectLastMonthRestockCount || 0), Number(total_monthly_restock || 0)).toFixed(2)),
|
||||
total_last_month_sales,
|
||||
total_monthly_sales,
|
||||
salesGrowth: Number(calculateGrowth(Number(total_last_month_sales || 0), Number(total_monthly_sales || 0)).toFixed(2)),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import createHttpError from "http-errors"
|
||||
import { makePrivateAutoPrediction } from "../predictionServices"
|
||||
import { IPredictionTable } from "../../types/db-model"
|
||||
|
||||
export async function buildAutoPrediction(
|
||||
csv_string: string,
|
||||
prediction_period: IPredictionTable['period_type']
|
||||
) {
|
||||
const result = await makePrivateAutoPrediction({
|
||||
csv_string,
|
||||
prediction_period,
|
||||
value_column: 'amount',
|
||||
})
|
||||
|
||||
if (!result) throw createHttpError(422, 'Prediksi gagal: model tidak mengembalikan hasil.')
|
||||
|
||||
if (result.mape > 50) {
|
||||
throw createHttpError(422, `Prediksi tidak layak karena MAPE terlalu tinggi (${result.mape.toFixed(2)}%). Harap periksa data input.`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import createHttpError from "http-errors"
|
||||
import { makePrivateManualPrediction } from "../predictionServices"
|
||||
import { IPredictionTable } from "../../types/db-model"
|
||||
|
||||
export async function buildManualPrediction(
|
||||
csv_string: string,
|
||||
prediction_period: IPredictionTable['period_type'],
|
||||
model: [number, number, number]
|
||||
) {
|
||||
const result = await makePrivateManualPrediction({
|
||||
csv_string,
|
||||
prediction_period,
|
||||
value_column: 'amount',
|
||||
arima_model: model
|
||||
})
|
||||
|
||||
if (!result) throw createHttpError(422, 'Prediksi gagal: model tidak mengembalikan hasil.')
|
||||
|
||||
if (
|
||||
result.prediction[0] < 1
|
||||
)
|
||||
throw createHttpError(422, 'Hasil prediksi tidak valid: nilai prediksi terlalu kecil atau negatif. Jika menggunakan data dummy, pastikan data tersebut tidak fluktuatif dan masih relevan dengan data asli. Jika tidak menggunakan data dummy, kemungkinan produk ini tidak dapat diprediksi secara akurat.');
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { selectPredictionModel } from "../../repository/predictionModelRepository"
|
||||
import { IPredictionTable } from "../../types/db-model"
|
||||
|
||||
export async function getPredictionModel(
|
||||
product_id: number,
|
||||
prediction_period: IPredictionTable['period_type'],
|
||||
source: IPredictionTable['prediction_source']
|
||||
) {
|
||||
const model = await selectPredictionModel(product_id, prediction_period, source)
|
||||
const isExpired = !model?.expired || new Date(model.expired) < new Date()
|
||||
|
||||
return { model, isExpired }
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { insertPrediction, selectPrediction, updatePrediction } from "../../repository/predictionRepository"
|
||||
import { IPredictionTable } from "../../types/db-model"
|
||||
import { TPythonAutoPredictionResponse, TPythonManualPredictionResponse } from "../../types/request/python-api"
|
||||
import { getExpiredDateFromMonth, getExpiredDateFromWeek } from "../../utils/core/date"
|
||||
|
||||
export async function persistPrediction(
|
||||
prediction_result: TPythonAutoPredictionResponse | TPythonManualPredictionResponse,
|
||||
prediction_period: IPredictionTable['period_type'],
|
||||
source: IPredictionTable['prediction_source'],
|
||||
product_id: IPredictionTable['product_id'],
|
||||
user_id: IPredictionTable['user_id']
|
||||
) {
|
||||
const previous = await selectPrediction(product_id, prediction_period, source)
|
||||
const expired = prediction_period === 'monthly' ?
|
||||
getExpiredDateFromMonth(new Date(), 1) :
|
||||
getExpiredDateFromWeek(new Date(), 1)
|
||||
|
||||
const record = {
|
||||
...previous,
|
||||
lower_bound: prediction_result.lower.map(v => Math.round(v))[0],
|
||||
prediction: prediction_result.prediction.map(v => Math.round(v))[0],
|
||||
upper_bound: prediction_result.upper.map(v => Math.round(v))[0],
|
||||
period_type: prediction_period,
|
||||
prediction_source: source,
|
||||
product_id,
|
||||
user_id,
|
||||
mape: Math.round((prediction_result as TPythonAutoPredictionResponse)?.mape || previous?.mape || 0),
|
||||
rmse: Math.round((prediction_result as TPythonAutoPredictionResponse)?.rmse || previous?.rmse || 0),
|
||||
expired
|
||||
}
|
||||
|
||||
if (!!previous?.id) {
|
||||
await updatePrediction(previous.id, record)
|
||||
} else {
|
||||
const id = await insertPrediction(record)
|
||||
record.id = id
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import createHttpError from 'http-errors'
|
||||
import papaparse from 'papaparse'
|
||||
import { selectGroupedMonthlyPurchase, selectGroupedWeeklyPurchase } from '../../repository/restockRepository'
|
||||
import { selectDummy } from '../../repository/dummyRepository'
|
||||
|
||||
export async function preparePredictionData(product_id: number, prediction_period: 'weekly' | 'monthly', source: 'purchases' | 'sales') {
|
||||
let groupedData
|
||||
if (prediction_period === 'weekly') {
|
||||
groupedData = await selectGroupedWeeklyPurchase(product_id)
|
||||
} else {
|
||||
groupedData = await selectGroupedMonthlyPurchase(product_id)
|
||||
}
|
||||
|
||||
if (!groupedData || groupedData.length === 0) {
|
||||
throw createHttpError(404, `Minimal lakukan 1 ${source === 'purchases' ? 'pembelian' : 'penjualan'} product dan lakukan prediksi di bulan berikutnya`)
|
||||
}
|
||||
|
||||
let data = groupedData.map(v => ({ amount: v.amount }))
|
||||
|
||||
if (groupedData.length < 10) {
|
||||
const dummy = await selectDummy(product_id, prediction_period, source)
|
||||
if (!dummy?.fake_json) {
|
||||
throw createHttpError(422, 'Prediksi tidak dapat dilakukan: data asli terlalu sedikit dan fallback data dummy tidak tersedia.')
|
||||
}
|
||||
|
||||
const fakeJSON = dummy.fake_json.filter(v => v >= 1).map(v => ({ amount: v }))
|
||||
if (fakeJSON.length + groupedData.length < 11) {
|
||||
throw createHttpError(422, 'Prediksi tidak dapat dilakukan: total data (asli + dummy) masih kurang dari 11 periode.')
|
||||
}
|
||||
|
||||
data.unshift(...fakeJSON)
|
||||
}
|
||||
|
||||
const csv_string = papaparse.unparse(data)
|
||||
return csv_string
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { insertPredictionModel, updatePredictionModel } from "../../repository/predictionModelRepository"
|
||||
import { IPredictionModelTable, IPredictionTable } from "../../types/db-model"
|
||||
import { TPythonAutoPredictionResponse, TPythonManualPredictionResponse } from "../../types/request/python-api"
|
||||
import { getExpiredDateFromMonth, getExpiredDateFromWeek } from "../../utils/core/date"
|
||||
|
||||
export async function savePredictionModel(
|
||||
model: IPredictionModelTable | undefined,
|
||||
prediction_result: TPythonAutoPredictionResponse | TPythonManualPredictionResponse,
|
||||
product_id: IPredictionTable['product_id'],
|
||||
prediction_period: IPredictionTable['period_type'],
|
||||
source: IPredictionTable['prediction_source'],
|
||||
user_id: IPredictionTable['user_id']
|
||||
) {
|
||||
const [ar_p, differencing_d, ma_q] = prediction_result.arima_order
|
||||
const expired = prediction_period === 'monthly' ?
|
||||
getExpiredDateFromMonth(new Date(), 3) :
|
||||
getExpiredDateFromWeek(new Date(), 3)
|
||||
|
||||
if (!model?.id) {
|
||||
await insertPredictionModel({
|
||||
ar_p,
|
||||
differencing_d,
|
||||
ma_q,
|
||||
expired,
|
||||
period_type: prediction_period,
|
||||
prediction_source: source,
|
||||
product_id,
|
||||
user_id,
|
||||
})
|
||||
} else {
|
||||
await updatePredictionModel(model.id, {
|
||||
ar_p,
|
||||
differencing_d,
|
||||
ma_q,
|
||||
expired,
|
||||
period_type: prediction_period,
|
||||
prediction_source: source,
|
||||
product_id,
|
||||
user_id,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { IPredictionModelTable } from "../../types/db-model";
|
||||
|
||||
export function shouldUseAutoModel(model: IPredictionModelTable | undefined, isExpired: boolean) {
|
||||
return !model || isExpired
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import axios from "axios";
|
||||
import { TGroupedProductSales } from "../types/db/product-sales";
|
||||
import { config } from "dotenv";
|
||||
import { TPythonAutoPredictionRequest, TPythonAutoPredictionResponse, TPythonManualPredictionRequest, TPythonManualPredictionResponse } from "../types/request/python-api";
|
||||
import { IPurchaseTable } from "../types/db-model";
|
||||
config()
|
||||
|
||||
export async function makeAutoPrediction(data: TPythonAutoPredictionRequest) {
|
||||
try {
|
||||
const response = await axios.post<TPythonAutoPredictionResponse>(`/predict/auto`, data, {
|
||||
method: 'post',
|
||||
baseURL: process.env.PYTHON_API_HOST,
|
||||
})
|
||||
|
||||
if (response.status === 200 && response.data.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
console.warn('Prediksi gagal atau response tidak sesuai:', response.data);
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Gagal memanggil /predict/auto:', error?.response?.data || error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeManualPrediction(data: TPythonManualPredictionRequest) {
|
||||
try {
|
||||
const response = await axios.post<TPythonManualPredictionResponse>(`/predict/manual`, data, {
|
||||
method: 'post',
|
||||
baseURL: process.env.PYTHON_API_HOST,
|
||||
})
|
||||
if (response.status === 200 && response.data.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
console.warn('Prediksi gagal atau response tidak sesuai:', response.data);
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Gagal memanggil /predict/manual:', error?.response?.data || error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function makePrivateAutoPrediction(data: TPythonAutoPredictionRequest) {
|
||||
try {
|
||||
const response = await axios.post<TPythonAutoPredictionResponse>(`/predict/private/auto`, data, {
|
||||
method: 'post',
|
||||
baseURL: process.env.PYTHON_API_HOST,
|
||||
})
|
||||
|
||||
if (response.status === 200 && response.data.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
console.warn('Prediksi gagal atau response tidak sesuai:', response.data);
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Gagal memanggil /predict/auto:', error?.response?.data || error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function makePrivateManualPrediction(data: TPythonManualPredictionRequest) {
|
||||
try {
|
||||
const response = await axios.post<TPythonManualPredictionResponse>(`/predict/private/manual`, data, {
|
||||
method: 'post',
|
||||
baseURL: process.env.PYTHON_API_HOST,
|
||||
})
|
||||
if (response.status === 200 && response.data.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
console.warn('Prediksi gagal atau response tidak sesuai:', response.data);
|
||||
return null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Gagal memanggil /predict/manual:', error?.response?.data || error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { countProducts, getAllProducts, insertProduct, selectProductById, selectProductByProductCode, selectProductByProductCodes, selectProductsByUserId } from "../repository/productsRepository"
|
||||
import { countProducts, getAllProducts, insertProduct, selectLowStockProducts, selectProductById, selectProductByProductCode, selectProductByProductCodes, selectProductsByUserId } from "../repository/productsRepository"
|
||||
import { IProductTable } from "../types/db-model"
|
||||
|
||||
export const createProduct = async (data: IProductTable) => {
|
||||
|
@ -50,4 +50,8 @@ export const getProductByProductCode = (
|
|||
export const getProductByProductCodes = (
|
||||
user_id: IProductTable['user_id'],
|
||||
product_codes: IProductTable['product_code'][]
|
||||
) => selectProductByProductCodes(product_codes, user_id)
|
||||
) => selectProductByProductCodes(product_codes, user_id)
|
||||
|
||||
export const getLowStockProducts = (user_id: IProductTable['user_id']) => {
|
||||
return selectLowStockProducts(user_id)
|
||||
}
|
|
@ -21,8 +21,9 @@ export async function addTransactionRecords(data: {
|
|||
price: Number(p.selling_price),
|
||||
})
|
||||
|
||||
const stockAfter = (Number(dataMatched?.amount) || 0) - p.stock
|
||||
updateProductById(p.id, {
|
||||
stock: (Number(dataMatched?.amount) || 0) - p.stock
|
||||
stock: stockAfter < 1 ? 0 : stockAfter
|
||||
})
|
||||
})
|
||||
return insertsDataToTransaction(mergedData)
|
||||
|
|
|
@ -24,7 +24,8 @@ export interface IProductTable {
|
|||
selling_price: number | null;
|
||||
product_category_id: number | null;
|
||||
user_id: number;
|
||||
deleted: boolean
|
||||
deleted: boolean;
|
||||
low_stock_limit: number;
|
||||
}
|
||||
|
||||
export interface ISupplierTable {
|
||||
|
@ -58,18 +59,34 @@ export interface ITransactionTable {
|
|||
|
||||
export interface IPredictionTable {
|
||||
id: number;
|
||||
stock_sold: number;
|
||||
stock_predicted: number;
|
||||
type: string;
|
||||
accuracy: number;
|
||||
user_id: number;
|
||||
prediction: number;
|
||||
lower_bound: number;
|
||||
upper_bound: number;
|
||||
rmse: number;
|
||||
mape: number;
|
||||
product_id: number;
|
||||
user_id: number;
|
||||
expired: string; // bisa juga pakai `Date` kalau langsung di-parse
|
||||
period_type: 'weekly' | 'monthly';
|
||||
prediction_source: 'sales' | 'purchases';
|
||||
}
|
||||
|
||||
export interface IUserFileTable {
|
||||
export interface IPredictionModelTable {
|
||||
id: number;
|
||||
file_name: string;
|
||||
saved_file_name: string;
|
||||
is_converted: boolean;
|
||||
ar_p: number;
|
||||
ma_q: number;
|
||||
differencing_d: number;
|
||||
period_type: 'weekly' | 'monthly';
|
||||
prediction_source: 'sales' | 'purchases';
|
||||
expired: string; // bisa juga pakai `Date` kalau langsung di-parse
|
||||
product_id: number;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export interface IDummiesTable {
|
||||
id: number;
|
||||
product_id: number;
|
||||
fake_json: number[] | null;
|
||||
period_type: 'weekly' | 'monthly';
|
||||
trx_type: 'sales' | 'purchases';
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { IProductTable, ITransactionTable } from "../db-model";
|
||||
|
||||
export type TGroupedProductSales = {
|
||||
group_date: string;
|
||||
product_name: IProductTable['product_name'];
|
||||
product_code: IProductTable['product_code'];
|
||||
product_id: ITransactionTable['product_id'];
|
||||
total_transactions: number;
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
type TPythonBasePredictionRequest = {
|
||||
csv_string: string
|
||||
prediction_period: "weekly" | "monthly"
|
||||
value_column: string
|
||||
date_column?: string
|
||||
date_regroup?: boolean
|
||||
}
|
||||
|
||||
type TPythonBasePredictionResponse = {
|
||||
arima_order: [number, number, number]
|
||||
upper: number[]
|
||||
lower: number[]
|
||||
prediction: number[]
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export type TPythonAutoPredictionRequest = TPythonBasePredictionRequest
|
||||
export type TPythonAutoPredictionResponse = TPythonBasePredictionResponse & {
|
||||
rmse: number
|
||||
mape: number
|
||||
}
|
||||
|
||||
export type TPythonManualPredictionResponse = TPythonBasePredictionResponse
|
||||
export type TPythonManualPredictionRequest = TPythonBasePredictionRequest & {
|
||||
arima_model?: [number, number, number]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import dayjs from 'dayjs'
|
||||
|
||||
export function getStartOfWeek(dateInput: string | Date) {
|
||||
const date = dayjs(dateInput)
|
||||
const day = date.day() === 0 ? 7 : date.day() // 1 = Senin, 7 = Minggu
|
||||
return date.subtract(day - 1, 'day').startOf('day')// Return: dayjs('YYYY-MM-Monday 00:00:00.000')
|
||||
}
|
||||
|
||||
export function getEndOfWeek(dateInput: string | Date) {
|
||||
const monday = getStartOfWeek(dateInput)
|
||||
return monday.add(6, 'day').endOf('day')// Return: dayjs('YYYY-MM-Sunday 23:59:59.999')
|
||||
}
|
||||
|
||||
export function getStartOfMonth(dateInput: string | Date) {
|
||||
return dayjs(dateInput).startOf('month') // awal bulan: YYYY-MM-01 00:00:00.000
|
||||
}
|
||||
|
||||
export function getEndOfMonth(dateInput: string | Date) {
|
||||
return dayjs(dateInput).endOf('month') // akhir bulan: YYYY-MM-lastDay 23:59:59.999
|
||||
}
|
||||
|
||||
export function getExpiredDateFromMonth(dateInput: string | Date, period = 1) {
|
||||
return getStartOfMonth(dateInput).add(period, 'month').endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
export function getExpiredDateFromWeek(dateInput: string | Date, period = 1) {
|
||||
return getStartOfWeek(dateInput).add(period, 'week').endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { HttpError } from 'http-errors'
|
||||
|
||||
export function isErrorInstanceOfHttpError(error: unknown): error is HttpError {
|
||||
return error instanceof Error && 'status' in error
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export function calculateGrowth(current: number, previous: number): number {
|
||||
if (previous === 0) return current === 0 ? 0 : 100;
|
||||
return ((current - previous) / previous) * 100;
|
||||
}
|
Loading…
Reference in New Issue