fix
This commit is contained in:
parent
0e416be400
commit
9d689bdad3
|
@ -1,12 +1,12 @@
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { TAPIResponse } from "../../types/core/http";
|
import { TAPIResponse } from "../../types/core/http";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { selectAllProductNPrediction } from "../../repository/predictionRepository";
|
import { selectAllProductNPrediction, selectDetailPrediction } from "../../repository/predictionRepository";
|
||||||
import { body, matchedData, param } from "express-validator";
|
import { body, matchedData, param } from "express-validator";
|
||||||
import { makeAutoPrediction } from "../../services/predictionServices";
|
import { makeAutoPrediction } from "../../services/predictionServices";
|
||||||
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler";
|
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler";
|
||||||
import { IPredictionTable } from "../../types/db-model";
|
import { IPredictionTable } from "../../types/db-model";
|
||||||
import { preparePredictionData } from "../../services/prediction/prepareData";
|
import { getPreparedCSVString, preparePredictionData } from "../../services/prediction/prepareData";
|
||||||
import { getPredictionModel } from "../../services/prediction/getPredictionModel";
|
import { getPredictionModel } from "../../services/prediction/getPredictionModel";
|
||||||
import { shouldUseAutoModel } from "../../services/prediction/shouldUseAutoModel";
|
import { shouldUseAutoModel } from "../../services/prediction/shouldUseAutoModel";
|
||||||
import { buildAutoPrediction } from "../../services/prediction/buildAutoPrediction";
|
import { buildAutoPrediction } from "../../services/prediction/buildAutoPrediction";
|
||||||
|
@ -14,6 +14,8 @@ import { savePredictionModel } from "../../services/prediction/savePredictionMod
|
||||||
import { buildManualPrediction } from "../../services/prediction/buildManualPrediction";
|
import { buildManualPrediction } from "../../services/prediction/buildManualPrediction";
|
||||||
import { persistPrediction } from "../../services/prediction/persistPrediction";
|
import { persistPrediction } from "../../services/prediction/persistPrediction";
|
||||||
import { isErrorInstanceOfHttpError } from "../../utils/core/httpError";
|
import { isErrorInstanceOfHttpError } from "../../utils/core/httpError";
|
||||||
|
import { sendResponseError } from "../../utils/api/responseError";
|
||||||
|
import { getTotalDaysInNextMonth } from "../../utils/core/date";
|
||||||
|
|
||||||
const periodArray = ['daily', 'weekly', 'monthly']
|
const periodArray = ['daily', 'weekly', 'monthly']
|
||||||
|
|
||||||
|
@ -65,6 +67,7 @@ const filePrediction = [
|
||||||
value_column,
|
value_column,
|
||||||
prediction_period,
|
prediction_period,
|
||||||
date_regroup: record_period !== prediction_period,
|
date_regroup: record_period !== prediction_period,
|
||||||
|
future_step: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!pythonResponse?.mape || pythonResponse.mape > 50) {
|
if (!pythonResponse?.mape || pythonResponse.mape > 50) {
|
||||||
|
@ -86,7 +89,7 @@ const filePrediction = [
|
||||||
message: 'Gagal melakukan prediksi. Silakan coba lagi atau cek data input.',
|
message: 'Gagal melakukan prediksi. Silakan coba lagi atau cek data input.',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(createHttpError(500, "Internal Server Error", { cause: error }));
|
next(sendResponseError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -106,8 +109,7 @@ const getSavedPurchasePredictions = [
|
||||||
}
|
}
|
||||||
res.json(result)
|
res.json(result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
next(sendResponseError(error));
|
||||||
next(createHttpError(500, "Internal Server Error", { error }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -127,8 +129,7 @@ const getSavedSalePredictions = [
|
||||||
}
|
}
|
||||||
res.json(result)
|
res.json(result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
next(sendResponseError(error));
|
||||||
next(createHttpError(500, "Internal Server Error", { error }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -153,7 +154,7 @@ const purchasePrediction = [
|
||||||
let predictionResult
|
let predictionResult
|
||||||
if (shouldUseAutoModel(model, isExpired)) {
|
if (shouldUseAutoModel(model, isExpired)) {
|
||||||
predictionResult = await buildAutoPrediction(csv_string, prediction_period)
|
predictionResult = await buildAutoPrediction(csv_string, prediction_period)
|
||||||
await savePredictionModel(model, predictionResult, product_id, prediction_period, source, req.user!.id)
|
await savePredictionModel(model, predictionResult, product_id, prediction_period, source, req.user!.id, 3)
|
||||||
} else {
|
} else {
|
||||||
const arimaModel: [number, number, number] = [model!.ar_p, model!.differencing_d, model!.ma_q]
|
const arimaModel: [number, number, number] = [model!.ar_p, model!.differencing_d, model!.ma_q]
|
||||||
predictionResult = await buildManualPrediction(csv_string, prediction_period, arimaModel)
|
predictionResult = await buildManualPrediction(csv_string, prediction_period, arimaModel)
|
||||||
|
@ -163,9 +164,7 @@ const purchasePrediction = [
|
||||||
|
|
||||||
res.status(200).json({ success: true, data: finalResult })
|
res.status(200).json({ success: true, data: finalResult })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isErrorInstanceOfHttpError(err))
|
next(sendResponseError(err))
|
||||||
return next(err)
|
|
||||||
return next(createHttpError(500, 'Internal Server Error', { cause: err }))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -190,7 +189,7 @@ const salesPrediction = [
|
||||||
let predictionResult
|
let predictionResult
|
||||||
if (shouldUseAutoModel(model, isExpired)) {
|
if (shouldUseAutoModel(model, isExpired)) {
|
||||||
predictionResult = await buildAutoPrediction(csv_string, prediction_period)
|
predictionResult = await buildAutoPrediction(csv_string, prediction_period)
|
||||||
await savePredictionModel(model, predictionResult, product_id, prediction_period, source, req.user!.id)
|
await savePredictionModel(model, predictionResult, product_id, prediction_period, source, req.user!.id, 3)
|
||||||
} else {
|
} else {
|
||||||
const arimaModel: [number, number, number] = [model!.ar_p, model!.differencing_d, model!.ma_q]
|
const arimaModel: [number, number, number] = [model!.ar_p, model!.differencing_d, model!.ma_q]
|
||||||
predictionResult = await buildManualPrediction(csv_string, prediction_period, arimaModel)
|
predictionResult = await buildManualPrediction(csv_string, prediction_period, arimaModel)
|
||||||
|
@ -200,9 +199,101 @@ const salesPrediction = [
|
||||||
|
|
||||||
res.status(200).json({ success: true, data: finalResult })
|
res.status(200).json({ success: true, data: finalResult })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isErrorInstanceOfHttpError(err))
|
next(sendResponseError(err))
|
||||||
return next(err)
|
}
|
||||||
return next(createHttpError(500, 'Internal Server Error', { cause: err }))
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const smartPrediction = [
|
||||||
|
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".'),
|
||||||
|
|
||||||
|
body('prediction_source')
|
||||||
|
.notEmpty().withMessage('prediction_source tidak boleh kosong.')
|
||||||
|
.isIn(['sales', 'purchases']).withMessage('prediction_source harus bernilai "sales" atau "purchases".'),
|
||||||
|
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { product_id, prediction_period, prediction_source } = matchedData(req)
|
||||||
|
const prediction_period_days = prediction_period === 'weekly' ? 7 : getTotalDaysInNextMonth(new Date())
|
||||||
|
|
||||||
|
const {
|
||||||
|
csv_string, data_freq
|
||||||
|
} = await getPreparedCSVString(product_id, prediction_period, prediction_source)
|
||||||
|
|
||||||
|
const { model, isExpired } = await getPredictionModel(
|
||||||
|
product_id,
|
||||||
|
prediction_period,
|
||||||
|
prediction_source
|
||||||
|
)
|
||||||
|
|
||||||
|
let predictionResult
|
||||||
|
if (shouldUseAutoModel(model, isExpired)) {
|
||||||
|
predictionResult = await buildAutoPrediction(
|
||||||
|
csv_string,
|
||||||
|
prediction_period,
|
||||||
|
data_freq === 'daily' ? prediction_period_days : 1
|
||||||
|
)
|
||||||
|
await savePredictionModel(
|
||||||
|
model,
|
||||||
|
predictionResult,
|
||||||
|
product_id,
|
||||||
|
prediction_period,
|
||||||
|
prediction_source,
|
||||||
|
req.user!.id,
|
||||||
|
data_freq === 'daily' ? 1 : 3
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const arimaModel: [number, number, number] = [model!.ar_p, model!.differencing_d, model!.ma_q]
|
||||||
|
predictionResult = await buildManualPrediction(
|
||||||
|
csv_string,
|
||||||
|
prediction_period,
|
||||||
|
arimaModel,
|
||||||
|
data_freq === 'daily' ? prediction_period_days : 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalResult = await persistPrediction(
|
||||||
|
predictionResult,
|
||||||
|
prediction_period,
|
||||||
|
prediction_source,
|
||||||
|
product_id,
|
||||||
|
req.user!.id
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ success: true, data: finalResult })
|
||||||
|
} catch (error) {
|
||||||
|
next(sendResponseError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const predictionDetail = [
|
||||||
|
body('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".'),
|
||||||
|
body('source_type')
|
||||||
|
.notEmpty().withMessage('source_type tidak boleh kosong.')
|
||||||
|
.isIn(['sales', 'purchases']).withMessage('source_type harus bernilai "sales" atau "purchases".'),
|
||||||
|
expressValidatorErrorHandler,
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { product_id, prediction_period, source_type } = matchedData(req)
|
||||||
|
const detailPrediction = await selectDetailPrediction(
|
||||||
|
product_id, prediction_period, source_type
|
||||||
|
)
|
||||||
|
res.status(200).json({ success: true, data: detailPrediction })
|
||||||
|
} catch (err) {
|
||||||
|
next(sendResponseError(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -212,5 +303,7 @@ export default {
|
||||||
getSavedPurchasePredictions,
|
getSavedPurchasePredictions,
|
||||||
getSavedSalePredictions,
|
getSavedSalePredictions,
|
||||||
salesPrediction,
|
salesPrediction,
|
||||||
purchasePrediction
|
purchasePrediction,
|
||||||
|
predictionDetail,
|
||||||
|
smartPrediction
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export function selectLatestPredictions(
|
||||||
.join('product_categories as pc', 'pr.product_category_id', 'pc.id')
|
.join('product_categories as pc', 'pr.product_category_id', 'pc.id')
|
||||||
.select(
|
.select(
|
||||||
'pr.product_name',
|
'pr.product_name',
|
||||||
|
'pr.stock',
|
||||||
'p.mape',
|
'p.mape',
|
||||||
'p.prediction',
|
'p.prediction',
|
||||||
'pc.category_name',
|
'pc.category_name',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { db } from "../database/MySQL";
|
import { db } from "../database/MySQL";
|
||||||
import { IPredictionTable, IProductTable, ITransactionTable } from "../types/db-model";
|
import { IPredictionModelTable, IPredictionTable, IProductTable, ITransactionTable } from "../types/db-model";
|
||||||
import { TGroupedProductSales } from "../types/db/product-sales";
|
import { TGroupedProductSales } from "../types/db/product-sales";
|
||||||
import { selectProductsByUserId } from "./productsRepository";
|
import { selectProductsByUserId } from "./productsRepository";
|
||||||
|
|
||||||
|
@ -86,4 +86,31 @@ export async function insertPrediction(data: Partial<IPredictionTable>) {
|
||||||
|
|
||||||
export function updatePrediction(id: IPredictionTable['id'], data: Partial<IPredictionTable>) {
|
export function updatePrediction(id: IPredictionTable['id'], data: Partial<IPredictionTable>) {
|
||||||
return db<IPredictionTable>('predictions').where({ id }).update(data)
|
return db<IPredictionTable>('predictions').where({ id }).update(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectDetailPrediction(
|
||||||
|
product_id: IPredictionTable['product_id'],
|
||||||
|
period_type: IPredictionTable['period_type'],
|
||||||
|
prediction_source: IPredictionTable['prediction_source'],
|
||||||
|
) {
|
||||||
|
return db<IProductTable>('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()]));
|
||||||
|
})
|
||||||
|
.where('products.id', product_id)
|
||||||
|
.select(
|
||||||
|
'products.product_code',
|
||||||
|
'products.product_name',
|
||||||
|
'products.stock',
|
||||||
|
'products.selling_price',
|
||||||
|
'products.buying_price',
|
||||||
|
'predictions.prediction',
|
||||||
|
'predictions.lower_bound',
|
||||||
|
'predictions.upper_bound',
|
||||||
|
'predictions.mape',
|
||||||
|
'predictions.rmse'
|
||||||
|
).first();
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { db } from "../database/MySQL";
|
import { db } from "../../database/MySQL";
|
||||||
import { IPurchaseTable } from "../types/db-model";
|
import { IPurchaseTable } from "../../types/db-model";
|
||||||
import { getStartOfMonth, getStartOfWeek } from "../utils/core/date";
|
import { getStartOfMonth, getStartOfWeek } from "../../utils/core/date";
|
||||||
|
|
||||||
export function insertsDataToRestock(data: Partial<IPurchaseTable>[]) {
|
export function insertsDataToRestock(data: Partial<IPurchaseTable>[]) {
|
||||||
return db<IPurchaseTable>('restocks').insert(data)
|
return db<IPurchaseTable>('restocks').insert(data)
|
||||||
|
@ -49,6 +49,27 @@ export const selectLastMonthRestockCount = (user_id: number) => {
|
||||||
}>();
|
}>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectGroupedDailyPurchase(
|
||||||
|
productId: IPurchaseTable['product_id'],
|
||||||
|
endOfPeriod: 'last-week' | 'last-month'
|
||||||
|
) {
|
||||||
|
let startOfDay
|
||||||
|
if (endOfPeriod === 'last-month')
|
||||||
|
startOfDay = getStartOfMonth(new Date()).toISOString()
|
||||||
|
else (endOfPeriod === 'last-week')
|
||||||
|
startOfDay = getStartOfWeek(new Date()).toISOString()
|
||||||
|
|
||||||
|
return db<IPurchaseTable>('restocks')
|
||||||
|
.select(
|
||||||
|
db.raw('DATE(buying_date) AS date'),
|
||||||
|
db.raw('SUM(amount) AS total_amount')
|
||||||
|
)
|
||||||
|
.where('product_id', productId)
|
||||||
|
.andWhere('buying_date', '<', startOfDay)
|
||||||
|
.groupByRaw('DATE(buying_date)')
|
||||||
|
.orderBy('date');
|
||||||
|
}
|
||||||
|
|
||||||
export function selectGroupedWeeklyPurchase(productId: IPurchaseTable['product_id']) {
|
export function selectGroupedWeeklyPurchase(productId: IPurchaseTable['product_id']) {
|
||||||
return db<IPurchaseTable>('restocks')
|
return db<IPurchaseTable>('restocks')
|
||||||
.select(
|
.select(
|
|
@ -1,6 +1,6 @@
|
||||||
import { db } from "../database/MySQL";
|
import { db } from "../../database/MySQL";
|
||||||
import { ITransactionTable } from "../types/db-model";
|
import { ITransactionTable } from "../../types/db-model";
|
||||||
import { getStartOfMonth, getStartOfWeek } from "../utils/core/date";
|
import { getStartOfMonth, getStartOfWeek } from "../../utils/core/date";
|
||||||
|
|
||||||
export function insertsDataToTransaction(data: Partial<ITransactionTable>[]) {
|
export function insertsDataToTransaction(data: Partial<ITransactionTable>[]) {
|
||||||
return db<ITransactionTable>('transactions').insert(data)
|
return db<ITransactionTable>('transactions').insert(data)
|
||||||
|
@ -49,6 +49,27 @@ export const selectCurrentMonthSales = async (user_id: number) => {
|
||||||
}>();
|
}>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectGroupedDailySales(
|
||||||
|
productId: ITransactionTable['product_id'],
|
||||||
|
endOfPeriod: 'last-week' | 'last-month'
|
||||||
|
) {
|
||||||
|
let startOfDay
|
||||||
|
if (endOfPeriod === 'last-month')
|
||||||
|
startOfDay = getStartOfMonth(new Date()).toISOString()
|
||||||
|
else (endOfPeriod === 'last-week')
|
||||||
|
startOfDay = getStartOfWeek(new Date()).toISOString()
|
||||||
|
|
||||||
|
return db<ITransactionTable>('transactions')
|
||||||
|
.select(
|
||||||
|
db.raw('DATE(transaction_date) AS date'),
|
||||||
|
db.raw('SUM(amount) AS total_amount')
|
||||||
|
)
|
||||||
|
.where('product_id', productId)
|
||||||
|
.andWhere('transaction_date', '<', startOfDay)
|
||||||
|
.groupByRaw('DATE(transaction_date)')
|
||||||
|
.orderBy('date');
|
||||||
|
}
|
||||||
|
|
||||||
export function selectGroupedWeeklySales(productId: ITransactionTable['product_id']) {
|
export function selectGroupedWeeklySales(productId: ITransactionTable['product_id']) {
|
||||||
return db('transactions')
|
return db('transactions')
|
||||||
.select(
|
.select(
|
||||||
|
@ -73,6 +94,7 @@ export function selectGroupedMonthlySales(productId: ITransactionTable['product_
|
||||||
.where('product_id', productId)
|
.where('product_id', productId)
|
||||||
.andWhere('transaction_date', '<', getStartOfMonth(new Date()).toISOString())
|
.andWhere('transaction_date', '<', getStartOfMonth(new Date()).toISOString())
|
||||||
|
|
||||||
|
|
||||||
.groupBy(['year', 'month'])
|
.groupBy(['year', 'month'])
|
||||||
.orderBy(['year', 'month'])
|
.orderBy(['year', 'month'])
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
import authRoute from '../../controller/api/authController';
|
import authRoute from '../../controller/api/authController';
|
||||||
|
import authenticate from '../../middleware/authMiddleware';
|
||||||
|
|
||||||
router.post('/register', authRoute.register);
|
router.post('/register', authRoute.register);
|
||||||
router.post('/login', authRoute.login);
|
router.post('/login', authRoute.login);
|
||||||
|
@ -10,6 +11,6 @@ router.patch('/forgot-password/:token', authRoute.forgotPasswordChangePassword);
|
||||||
router.get('/verify/:token', authRoute.verify);
|
router.get('/verify/:token', authRoute.verify);
|
||||||
router.post('/re-send-email-activation/:token', authRoute.resendEmailVerification);
|
router.post('/re-send-email-activation/:token', authRoute.resendEmailVerification);
|
||||||
router.get('/refresh-token', authRoute.refreshToken);
|
router.get('/refresh-token', authRoute.refreshToken);
|
||||||
router.get('/logout', authRoute.logout);
|
router.get('/logout', authenticate, authRoute.logout);
|
||||||
|
|
||||||
export default router
|
export default router
|
|
@ -8,4 +8,6 @@ router.get('/saved-predictions/purchases/:period_type', authenticate, prediction
|
||||||
router.get('/saved-predictions/sales/:period_type', authenticate, predictionController.getSavedSalePredictions)
|
router.get('/saved-predictions/sales/:period_type', authenticate, predictionController.getSavedSalePredictions)
|
||||||
router.post('/purchase-prediction/:product_id', authenticate, predictionController.purchasePrediction)
|
router.post('/purchase-prediction/:product_id', authenticate, predictionController.purchasePrediction)
|
||||||
router.post('/sales-prediction/:product_id', authenticate, predictionController.salesPrediction)
|
router.post('/sales-prediction/:product_id', authenticate, predictionController.salesPrediction)
|
||||||
|
router.post('/smart-prediction/:product_id', authenticate, predictionController.smartPrediction)
|
||||||
|
router.post('/detail-prediction', authenticate, predictionController.predictionDetail)
|
||||||
export default router
|
export default router
|
|
@ -1,6 +1,6 @@
|
||||||
import { countProducts, selectLowStockProductCount } from "../repository/productsRepository"
|
import { countProducts, selectLowStockProductCount } from "../repository/productsRepository"
|
||||||
import { selectLastMonthRestockCount, selectMonthlyRestockCount } from "../repository/restockRepository"
|
import { selectLastMonthRestockCount, selectMonthlyRestockCount } from "../repository/transaction/purchase"
|
||||||
import { selectLastMonthSales, selectCurrentMonthSales } from "../repository/transactionRepository"
|
import { selectCurrentMonthSales, selectLastMonthSales } from "../repository/transaction/sales"
|
||||||
import { calculateGrowth } from '../utils/math/calculateGrowth'
|
import { calculateGrowth } from '../utils/math/calculateGrowth'
|
||||||
|
|
||||||
export async function getDashboardData(user_id: number) {
|
export async function getDashboardData(user_id: number) {
|
||||||
|
|
|
@ -4,12 +4,14 @@ import { IPredictionTable } from "../../types/db-model"
|
||||||
|
|
||||||
export async function buildAutoPrediction(
|
export async function buildAutoPrediction(
|
||||||
csv_string: string,
|
csv_string: string,
|
||||||
prediction_period: IPredictionTable['period_type']
|
prediction_period: IPredictionTable['period_type'],
|
||||||
|
future_step: number = 1
|
||||||
) {
|
) {
|
||||||
const result = await makePrivateAutoPrediction({
|
const result = await makePrivateAutoPrediction({
|
||||||
csv_string,
|
csv_string,
|
||||||
prediction_period,
|
prediction_period,
|
||||||
value_column: 'amount',
|
value_column: 'amount',
|
||||||
|
future_step
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) throw createHttpError(422, 'Prediksi gagal: model tidak mengembalikan hasil.')
|
if (!result) throw createHttpError(422, 'Prediksi gagal: model tidak mengembalikan hasil.')
|
||||||
|
|
|
@ -5,13 +5,15 @@ import { IPredictionTable } from "../../types/db-model"
|
||||||
export async function buildManualPrediction(
|
export async function buildManualPrediction(
|
||||||
csv_string: string,
|
csv_string: string,
|
||||||
prediction_period: IPredictionTable['period_type'],
|
prediction_period: IPredictionTable['period_type'],
|
||||||
model: [number, number, number]
|
model: [number, number, number],
|
||||||
|
future_step: number = 1
|
||||||
) {
|
) {
|
||||||
const result = await makePrivateManualPrediction({
|
const result = await makePrivateManualPrediction({
|
||||||
csv_string,
|
csv_string,
|
||||||
prediction_period,
|
prediction_period,
|
||||||
value_column: 'amount',
|
value_column: 'amount',
|
||||||
arima_model: model
|
arima_model: model,
|
||||||
|
future_step
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) throw createHttpError(422, 'Prediksi gagal: model tidak mengembalikan hasil.')
|
if (!result) throw createHttpError(422, 'Prediksi gagal: model tidak mengembalikan hasil.')
|
||||||
|
|
|
@ -17,9 +17,9 @@ export async function persistPrediction(
|
||||||
|
|
||||||
const record = {
|
const record = {
|
||||||
...previous,
|
...previous,
|
||||||
lower_bound: prediction_result.lower.map(v => Math.round(v))[0],
|
lower_bound: prediction_result.lower.map(v => Math.round(v), 0),
|
||||||
prediction: prediction_result.prediction.map(v => Math.round(v))[0],
|
prediction: prediction_result.prediction.map(v => Math.round(v)),
|
||||||
upper_bound: prediction_result.upper.map(v => Math.round(v))[0],
|
upper_bound: prediction_result.upper.map(v => Math.round(v)),
|
||||||
period_type: prediction_period,
|
period_type: prediction_period,
|
||||||
prediction_source: source,
|
prediction_source: source,
|
||||||
product_id,
|
product_id,
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
import createHttpError from 'http-errors'
|
import createHttpError from 'http-errors'
|
||||||
import papaparse from 'papaparse'
|
import papaparse from 'papaparse'
|
||||||
import { selectGroupedMonthlyPurchase, selectGroupedWeeklyPurchase } from '../../repository/restockRepository'
|
|
||||||
import { selectDummy } from '../../repository/dummyRepository'
|
import { selectDummy } from '../../repository/dummyRepository'
|
||||||
|
import { selectGroupedDailyPurchase, selectGroupedMonthlyPurchase, selectGroupedWeeklyPurchase } from '../../repository/transaction/purchase'
|
||||||
|
import { selectGroupedDailySales, selectGroupedMonthlySales, selectGroupedWeeklySales } from '../../repository/transaction/sales'
|
||||||
|
|
||||||
export async function preparePredictionData(product_id: number, prediction_period: 'weekly' | 'monthly', source: 'purchases' | 'sales') {
|
export async function preparePredictionData(product_id: number, prediction_period: 'weekly' | 'monthly', source: 'purchases' | 'sales') {
|
||||||
let groupedData
|
let groupedData
|
||||||
if (prediction_period === 'weekly') {
|
if (prediction_period === 'weekly') {
|
||||||
groupedData = await selectGroupedWeeklyPurchase(product_id)
|
if (source === 'purchases') {
|
||||||
} else {
|
groupedData = await selectGroupedWeeklyPurchase(product_id)
|
||||||
groupedData = await selectGroupedMonthlyPurchase(product_id)
|
} else if (source === 'sales') {
|
||||||
|
groupedData = await selectGroupedWeeklySales(product_id)
|
||||||
|
}
|
||||||
|
} else if (prediction_period === 'monthly') {
|
||||||
|
if (source === 'purchases') {
|
||||||
|
groupedData = await selectGroupedMonthlyPurchase(product_id)
|
||||||
|
} else if (source === 'sales') {
|
||||||
|
groupedData = await selectGroupedMonthlySales(product_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupedData || groupedData.length === 0) {
|
if (!groupedData || groupedData.length === 0) {
|
||||||
throw createHttpError(404, `Minimal lakukan 1 ${source === 'purchases' ? 'pembelian' : 'penjualan'} product dan lakukan prediksi di bulan berikutnya`)
|
throw createHttpError(404, `Minimal lakukan 1 ${source === 'purchases' ? 'pembelian' : 'penjualan'} product dan lakukan prediksi di ${prediction_period === 'monthly' ? 'bulan' : 'minggu'} berikutnya`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = groupedData.map(v => ({ amount: v.amount }))
|
let data = groupedData.map(v => ({ amount: v.amount }))
|
||||||
|
@ -34,3 +43,58 @@ export async function preparePredictionData(product_id: number, prediction_perio
|
||||||
const csv_string = papaparse.unparse(data)
|
const csv_string = papaparse.unparse(data)
|
||||||
return csv_string
|
return csv_string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PreparedDataReturnValue = {
|
||||||
|
data_freq: 'daily' | 'weekly' | 'monthly'
|
||||||
|
csv_string: string
|
||||||
|
}
|
||||||
|
export async function getPreparedCSVString(
|
||||||
|
product_id: number,
|
||||||
|
prediction_period: 'weekly' | 'monthly',
|
||||||
|
source: 'purchases' | 'sales'
|
||||||
|
): Promise<PreparedDataReturnValue> {
|
||||||
|
const returnValue: PreparedDataReturnValue = {
|
||||||
|
data_freq: prediction_period,
|
||||||
|
csv_string: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupedData
|
||||||
|
if (prediction_period === 'weekly') {
|
||||||
|
if (source === 'purchases') {
|
||||||
|
groupedData = await selectGroupedWeeklyPurchase(product_id)
|
||||||
|
} else {
|
||||||
|
groupedData = await selectGroupedWeeklySales(product_id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (source === 'purchases') {
|
||||||
|
groupedData = await selectGroupedMonthlyPurchase(product_id)
|
||||||
|
} else {
|
||||||
|
groupedData = await selectGroupedMonthlySales(product_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupedData.length < 10) {
|
||||||
|
if (source === 'purchases') {
|
||||||
|
groupedData = await selectGroupedDailyPurchase(
|
||||||
|
product_id,
|
||||||
|
prediction_period === 'weekly' ? 'last-week' : 'last-month'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
groupedData = await selectGroupedDailySales(
|
||||||
|
product_id,
|
||||||
|
prediction_period === 'weekly' ? 'last-week' : 'last-month'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
returnValue.data_freq = 'daily'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupedData.length < 30) {
|
||||||
|
throw createHttpError(422, `Minimal lakukan 30 transaksi ${source === 'sales' ? 'penjualan' : 'pembelian'} harian pada ${prediction_period === 'weekly' ? 'minggu' : 'bulan'} sebelumnya untuk dapat melakukan prediksi`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = groupedData.map(v => ({ amount: v.amount }))
|
||||||
|
|
||||||
|
returnValue.csv_string = papaparse.unparse(data)
|
||||||
|
|
||||||
|
return returnValue
|
||||||
|
}
|
|
@ -9,12 +9,13 @@ export async function savePredictionModel(
|
||||||
product_id: IPredictionTable['product_id'],
|
product_id: IPredictionTable['product_id'],
|
||||||
prediction_period: IPredictionTable['period_type'],
|
prediction_period: IPredictionTable['period_type'],
|
||||||
source: IPredictionTable['prediction_source'],
|
source: IPredictionTable['prediction_source'],
|
||||||
user_id: IPredictionTable['user_id']
|
user_id: IPredictionTable['user_id'],
|
||||||
|
expiration_interval: number
|
||||||
) {
|
) {
|
||||||
const [ar_p, differencing_d, ma_q] = prediction_result.arima_order
|
const [ar_p, differencing_d, ma_q] = prediction_result.arima_order
|
||||||
const expired = prediction_period === 'monthly' ?
|
const expired = prediction_period === 'monthly' ?
|
||||||
getExpiredDateFromMonth(new Date(), 3) :
|
getExpiredDateFromMonth(new Date(), expiration_interval) :
|
||||||
getExpiredDateFromWeek(new Date(), 3)
|
getExpiredDateFromWeek(new Date(), expiration_interval)
|
||||||
|
|
||||||
if (!model?.id) {
|
if (!model?.id) {
|
||||||
await insertPredictionModel({
|
await insertPredictionModel({
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { countRestocks, insertsDataToRestock, selectAllRestockHistory } from "../repository/restockRepository";
|
import { countRestocks, insertsDataToRestock, selectAllRestockHistory } from "../repository/transaction/purchase";
|
||||||
import { IProductTable, IPurchaseTable } from "../types/db-model";
|
import { IProductTable, IPurchaseTable } from "../types/db-model";
|
||||||
import { getProductByProductCodes, updateProductById } from "./productServices";
|
import { getProductByProductCodes, updateProductById } from "./productServices";
|
||||||
|
|
||||||
|
@ -51,4 +51,4 @@ export async function showRestockHistory(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { countSales, insertsDataToTransaction, selectAllSalesHistory } from "../repository/transactionRepository";
|
import { countSales, insertsDataToTransaction, selectAllSalesHistory } from "../repository/transaction/sales";
|
||||||
import { IProductTable, ITransactionTable } from "../types/db-model";
|
import { IProductTable, ITransactionTable } from "../types/db-model";
|
||||||
import { getProductByProductCodes, updateProductById } from "./productServices";
|
import { getProductByProductCodes, updateProductById } from "./productServices";
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export async function addTransactionRecords(data: {
|
||||||
price: Number(p.selling_price),
|
price: Number(p.selling_price),
|
||||||
})
|
})
|
||||||
|
|
||||||
const stockAfter = (Number(dataMatched?.amount) || 0) - p.stock
|
const stockAfter = p.stock - (Number(dataMatched?.amount) || 0)
|
||||||
updateProductById(p.id, {
|
updateProductById(p.id, {
|
||||||
stock: stockAfter < 1 ? 0 : stockAfter
|
stock: stockAfter < 1 ? 0 : stockAfter
|
||||||
})
|
})
|
||||||
|
|
|
@ -59,9 +59,9 @@ export interface ITransactionTable {
|
||||||
|
|
||||||
export interface IPredictionTable {
|
export interface IPredictionTable {
|
||||||
id: number;
|
id: number;
|
||||||
prediction: number;
|
prediction: number[];
|
||||||
lower_bound: number;
|
lower_bound: number[];
|
||||||
upper_bound: number;
|
upper_bound: number[];
|
||||||
rmse: number;
|
rmse: number;
|
||||||
mape: number;
|
mape: number;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
|
|
|
@ -4,6 +4,7 @@ type TPythonBasePredictionRequest = {
|
||||||
value_column: string
|
value_column: string
|
||||||
date_column?: string
|
date_column?: string
|
||||||
date_regroup?: boolean
|
date_regroup?: boolean
|
||||||
|
future_step: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type TPythonBasePredictionResponse = {
|
type TPythonBasePredictionResponse = {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import createHttpError, { HttpError } from "http-errors";
|
||||||
|
import { isErrorInstanceOfHttpError } from "../core/httpError";
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
export function sendResponseError(error: unknown): HttpError {
|
||||||
|
let newError: HttpError;
|
||||||
|
if (isErrorInstanceOfHttpError(error)) {
|
||||||
|
newError = error;
|
||||||
|
} else {
|
||||||
|
newError = createHttpError(500, "Internal Server Error", { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error(newError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newError;
|
||||||
|
}
|
|
@ -25,4 +25,8 @@ export function getExpiredDateFromMonth(dateInput: string | Date, period = 1) {
|
||||||
|
|
||||||
export function getExpiredDateFromWeek(dateInput: string | Date, period = 1) {
|
export function getExpiredDateFromWeek(dateInput: string | Date, period = 1) {
|
||||||
return getStartOfWeek(dateInput).add(period, 'week').endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
return getStartOfWeek(dateInput).add(period, 'week').endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTotalDaysInNextMonth(dateInput: string | Date = new Date()) {
|
||||||
|
return getStartOfMonth(dateInput).add(1, 'month').daysInMonth()
|
||||||
}
|
}
|
Loading…
Reference in New Issue