backup
This commit is contained in:
parent
cd198e9272
commit
da539298fb
|
@ -8,6 +8,7 @@
|
|||
"name": "order-services-node",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@ngrok/ngrok": "^1.5.1",
|
||||
"amqplib": "^0.10.3",
|
||||
"axios": "^1.4.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
@ -158,6 +159,235 @@
|
|||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-1.5.1.tgz",
|
||||
"integrity": "sha512-sfcgdpiAJHqmuO3e6QjQGbavIrR3E72do/NAsnGhm+7SGstLj1aM3Sd8mkfTORb2Hj7ATMuoBYuED5ylKuRQCg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ngrok/ngrok-android-arm64": "1.5.1",
|
||||
"@ngrok/ngrok-darwin-arm64": "1.5.1",
|
||||
"@ngrok/ngrok-darwin-universal": "1.5.1",
|
||||
"@ngrok/ngrok-darwin-x64": "1.5.1",
|
||||
"@ngrok/ngrok-freebsd-x64": "1.5.1",
|
||||
"@ngrok/ngrok-linux-arm-gnueabihf": "1.5.1",
|
||||
"@ngrok/ngrok-linux-arm64-gnu": "1.5.1",
|
||||
"@ngrok/ngrok-linux-arm64-musl": "1.5.1",
|
||||
"@ngrok/ngrok-linux-x64-gnu": "1.5.1",
|
||||
"@ngrok/ngrok-linux-x64-musl": "1.5.1",
|
||||
"@ngrok/ngrok-win32-arm64-msvc": "1.5.1",
|
||||
"@ngrok/ngrok-win32-ia32-msvc": "1.5.1",
|
||||
"@ngrok/ngrok-win32-x64-msvc": "1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-android-arm64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm64/-/ngrok-android-arm64-1.5.1.tgz",
|
||||
"integrity": "sha512-2Tokwi5GVWNLw3JEoM0Ieb/ypALniZu6fciUTgpuByutbKxOjvahD4fYOKwW3KMdV9bCb3XGGtWJCZXfRPPq1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-darwin-arm64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-arm64/-/ngrok-darwin-arm64-1.5.1.tgz",
|
||||
"integrity": "sha512-HNOhrPDP+nJJY7Bh45DOeh6jmcGASWINGbUuseZM0C8psQMp7crPywjRh0inkRegUrb4K8y06sfmgt2fmsF6jQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-darwin-universal": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-universal/-/ngrok-darwin-universal-1.5.1.tgz",
|
||||
"integrity": "sha512-EsMxYC/tY+ZqhjbeZtVq5MFIuD8SEPgAlHINEszsHd8ZRICc2U9Xl15CbDrew3pcfEg/ZVFrOH9CyC4aZ/V/cA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-darwin-x64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-x64/-/ngrok-darwin-x64-1.5.1.tgz",
|
||||
"integrity": "sha512-H/x1BsYpAoTMhOtv4oYvwY6WHqbY0MsJ1XFcJQgrpAIjgmYqlwsnsUMHvEdBB/KY9kXF9DPgKUdRMfJwUIpwGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-freebsd-x64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-freebsd-x64/-/ngrok-freebsd-x64-1.5.1.tgz",
|
||||
"integrity": "sha512-dY2W6HUv7e2xkpdfVj7fIk+5qmvrC7kVu6PJWJ8/rshW1FrU7qMcpnU53JvoQJRZzUf5k8xMNdx30zai/8mqYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-arm-gnueabihf": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm-gnueabihf/-/ngrok-linux-arm-gnueabihf-1.5.1.tgz",
|
||||
"integrity": "sha512-JvbI/IIycw4Qq02ysyOBsSK5E0bZDgRqXSslHLTwuDAfw14lmrq2U0QkBeEOL8qwJ7wCwCH1PEOJacUyrqa9bg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-arm64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-gnu/-/ngrok-linux-arm64-gnu-1.5.1.tgz",
|
||||
"integrity": "sha512-yLFAlqTYYvH7QRg589HJarQGw1QrKQZcHiw0gm175eCqc+jpUG/Zcf8wohCTIJVLylMIzjDzVFSUsXC7UtMJdQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-arm64-musl": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-musl/-/ngrok-linux-arm64-musl-1.5.1.tgz",
|
||||
"integrity": "sha512-momB/ZjjrxaGYOZ3YPAw1kT4DAfWT1x3dAHL0YoSVfNCpc8Fw0189ZAcxGn0hUFqkGDmSARS9o8b7hYd1b41oA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-x64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-gnu/-/ngrok-linux-x64-gnu-1.5.1.tgz",
|
||||
"integrity": "sha512-fmMaz0b1Ry2CDLLn0mV8b9nLxqm0taQ2jYyn+C9OrazYNMT4XYYDKRQSm4UEaNoakdnoH+f2FsrWi/712GFxAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-x64-musl": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-musl/-/ngrok-linux-x64-musl-1.5.1.tgz",
|
||||
"integrity": "sha512-6Ajl9wpJSlvukl4WrkIw+WxVwAr7WTGnE35Voec6CERWtKMsO/F+BOSu3pfAa6iwxGK//JBpsTT1IwLLw7b2xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-win32-arm64-msvc": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-arm64-msvc/-/ngrok-win32-arm64-msvc-1.5.1.tgz",
|
||||
"integrity": "sha512-JUH2yZxDPQGmQNT1d2KIu64u2k/R6uG1kEIXjcbsoff37v9aI6nUlzldRWB/wFSYkpZ4W/EuovM4Epar+fQOxQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-win32-ia32-msvc": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-ia32-msvc/-/ngrok-win32-ia32-msvc-1.5.1.tgz",
|
||||
"integrity": "sha512-zS1JsMTJHnY+lPJFUwKnB5fzPm4GZCKeeZLehHrXP0LpQaKN8Y/vywqDGhuC0WtymvWE88+oreMV/6hQdviLSA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-win32-x64-msvc": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-x64-msvc/-/ngrok-win32-x64-msvc-1.5.1.tgz",
|
||||
"integrity": "sha512-HegRwV9Gchh4p7K7sC6SPpWmFRwDEgwPByrb8tkuWDyP+EWNgpt3GKp8OAIK2xdWWHnN5VIwMa9u3COE/e5S8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"build": "tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ngrok/ngrok": "^1.5.1",
|
||||
"amqplib": "^0.10.3",
|
||||
"axios": "^1.4.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
|
74
src/app.ts
74
src/app.ts
|
@ -10,31 +10,40 @@ import helmet from 'helmet';
|
|||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
|
||||
// import internalRouter from './routes/internal';
|
||||
import apiRouter from './routes/api';
|
||||
import connectDB from './database/MySQL';
|
||||
import { root_path } from './utils/core/storage';
|
||||
import { respondWithError } from './middleware/core/errorHandler';
|
||||
// import schedule from 'node-schedule';
|
||||
import { getLocalIP } from './dev-core';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
|
||||
const localIP = getLocalIP()
|
||||
|
||||
app.use(cors({
|
||||
// 🚨 1. Setup Allowed Origins
|
||||
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com', `http://${localIP}:3000`];
|
||||
|
||||
const corsOptions: cors.CorsOptions = {
|
||||
origin(origin, callback) {
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
} else {
|
||||
return callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
if (!origin) return callback(null, true); // Allow server-to-server or curl
|
||||
if (allowedOrigins.includes(origin)) return callback(null, true);
|
||||
return callback(new Error('Not allowed by CORS'));
|
||||
},
|
||||
credentials: true
|
||||
}));
|
||||
credentials: true,
|
||||
};
|
||||
|
||||
// 🚨 2. Pasang CORS Middleware DULUAN
|
||||
app.use(cors(corsOptions));
|
||||
// 💡 3. Handle all OPTIONS preflight
|
||||
app.options('*', cors(corsOptions));
|
||||
|
||||
// 🔍 4. Logging semua request (termasuk OPTIONS)
|
||||
app.use((req, _res, next) => {
|
||||
console.log(`[${req.method}] ${req.originalUrl}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// 💨 Middleware lain
|
||||
app.use(compression());
|
||||
app.use(helmet());
|
||||
app.use(logger('dev'));
|
||||
|
@ -43,40 +52,35 @@ app.use(express.urlencoded({ extended: false }));
|
|||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// View engine setup
|
||||
// app.set('views', path.join(__dirname, 'views'));
|
||||
// 🖼️ View engine
|
||||
app.set('views', root_path('src/views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Database connection
|
||||
// 🔌 DB connection
|
||||
connectDB();
|
||||
|
||||
app.get('/', function (req, res, next) {
|
||||
// ✅ Basic endpoint untuk cek server hidup
|
||||
app.get('/', function (req, res) {
|
||||
res.json({
|
||||
status: 'running',
|
||||
developer: 'Fahim',
|
||||
project: 'Stock Prediction with ARIMA'
|
||||
project: 'Stock Prediction with ARIMA',
|
||||
});
|
||||
});
|
||||
|
||||
// 🔗 API Router
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
// 404 handler (opsional, bisa diaktifkan jika perlu)
|
||||
// app.use((_req, _res, next) => {
|
||||
// next(createHttpError(404));
|
||||
// });
|
||||
|
||||
// Scheduler (opsional, jika ingin dijalankan otomatis)
|
||||
// const scheduleMidnight = schedule.scheduleJob('0 0 * * *', async () => {
|
||||
// const result = await runningSchedule() ? 'success running midnight schedule' : 'failed running midnight schedule';
|
||||
// console.log(result);
|
||||
// });
|
||||
|
||||
// const scheduleNoon = schedule.scheduleJob('0 12 * * *', async () => {
|
||||
// const result = await runningSchedule() ? 'success running noon schedule' : 'failed running noon schedule';
|
||||
// console.log(result);
|
||||
// });
|
||||
// 🚨 5. Tangani CORS error secara eksplisit
|
||||
app.use(((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
if (err.message === 'Not allowed by CORS') {
|
||||
console.error('❌ CORS error:', req.headers.origin);
|
||||
return res.status(403).json({ error: 'CORS policy does not allow this origin.' });
|
||||
}
|
||||
next(err);
|
||||
}) as express.ErrorRequestHandler);
|
||||
|
||||
// ❗ 6. Global error handler (dari kamu)
|
||||
app.use(respondWithError);
|
||||
|
||||
export default app;
|
||||
export default app;
|
|
@ -1,94 +1,99 @@
|
|||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import { body, matchedData, param } from 'express-validator';
|
||||
import createHttpError from 'http-errors';
|
||||
import { product_categories } from '../../database/models';
|
||||
import expressValidatorErrorHandler from '../../middleware/expressValidatorErrorHandler';
|
||||
import { body, matchedData, param } from "express-validator";
|
||||
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { IProductCategoryTable } from "../../types/db-model";
|
||||
import { createProductCategory, deleteProductCategory, showAllProductCategory, updateProductCategoryById } from "../../services/productCategoryServices";
|
||||
import createHttpError from "http-errors";
|
||||
import { TAPIResponse } from "../../types/core/http";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// POST: Tambah kategori baru
|
||||
const createCategory = [
|
||||
body("category_name").notEmpty().withMessage("Category name is required"),
|
||||
body("description").optional().isString(),
|
||||
body("user_id").notEmpty().withMessage("User ID is required").isMongoId(),
|
||||
const addProductCategory = [
|
||||
body("category_name").notEmpty().isString(),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const reqBody = matchedData<IProductCategoryTable>(req)
|
||||
try {
|
||||
const data = matchedData(req);
|
||||
const category = new product_categories(data);
|
||||
await category.save();
|
||||
res.status(201).json({ message: "Category created", category });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
const newProductId = await createProductCategory({
|
||||
...reqBody,
|
||||
category_name: reqBody.category_name.toLowerCase(),
|
||||
user_id: req.user!.id
|
||||
})
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'New product category successfully created.',
|
||||
data: {
|
||||
productCategoryId: newProductId
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error: unknown) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// PATCH: Perbarui kategori
|
||||
const updateCategory = [
|
||||
param("id").isMongoId().withMessage("Invalid category ID"),
|
||||
body("category_name").optional().isString(),
|
||||
body("description").optional().isString(),
|
||||
expressValidatorErrorHandler,
|
||||
}
|
||||
]
|
||||
|
||||
const getProductCategories = [
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const data = matchedData(req);
|
||||
const category = await product_categories.findByIdAndUpdate(id, data, { new: true });
|
||||
if (!category) return next(createHttpError(404, "Category not found"));
|
||||
res.json({ message: "Category updated", category });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
const productCategories = await showAllProductCategory(req.user!.id)
|
||||
console.log(productCategories)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: productCategories
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// DELETE: Hapus kategori
|
||||
const deleteCategory = [
|
||||
param("id").isMongoId().withMessage("Invalid category ID"),
|
||||
const deleteProductCategoryRoute = [
|
||||
param('id'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const category = await product_categories.findByIdAndDelete(id);
|
||||
if (!category) return next(createHttpError(404, "Category not found"));
|
||||
res.json({ message: "Category deleted" });
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
const { id } = matchedData(req)
|
||||
await deleteProductCategory(id, req.user!.id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Product category successfully deleted'
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// GET: Ambil satu kategori berdasarkan ID
|
||||
const getCategory = [
|
||||
param("id").isMongoId().withMessage("Invalid category ID"),
|
||||
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 { id } = req.params;
|
||||
const category = await product_categories.findById(id);
|
||||
if (!category) return next(createHttpError(404, "Category not found"));
|
||||
res.json(category);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
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))
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
// GET: Ambil semua kategori
|
||||
const getCategories = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const categories = await product_categories.find();
|
||||
res.json(categories);
|
||||
} catch (err) {
|
||||
next(createHttpError(500, "Internal Server Error", { cause: err }));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
createCategory,
|
||||
updateCategory,
|
||||
deleteCategory,
|
||||
getCategory,
|
||||
getCategories,
|
||||
};
|
||||
addProductCategory, getProductCategories, deleteProductCategoryRoute
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
import { body, matchedData, param, query } from "express-validator"
|
||||
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler"
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
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"
|
||||
|
||||
const addProduct = [
|
||||
body('product_code').notEmpty().isString(),
|
||||
body('product_name').notEmpty().isString(),
|
||||
body('stock')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('selling_price')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('buying_price')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('product_category_id')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const reqData = matchedData<IProductTable>(req)
|
||||
try {
|
||||
const newProductId = await createProduct({
|
||||
...reqData,
|
||||
buying_price: reqData.buying_price || null,
|
||||
selling_price: reqData.selling_price || null,
|
||||
stock: reqData.stock || 0,
|
||||
product_category_id: reqData.product_category_id || null,
|
||||
user_id: req.user!.id
|
||||
})
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'New product successfully created.',
|
||||
data: {
|
||||
productId: newProductId
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const showAllProducts = [
|
||||
query('page').optional().isInt(),
|
||||
query('limit').optional().isInt(),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { page = 1, limit = 10 } = matchedData(req)
|
||||
const products = await getProducts(req.user!.id, page, limit);
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: products
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const getProductDetail = [
|
||||
param('id'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = matchedData(req)
|
||||
const product = await showProductById(id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
data: product
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const updateProductRoute = [
|
||||
param('id').notEmpty(),
|
||||
body('product_code').notEmpty().isString(),
|
||||
body('product_name').notEmpty().isString(),
|
||||
body('stock')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('selling_price')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('buying_price')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('product_category_id')
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = matchedData(req, { locations: ['params'] })
|
||||
const reqBody = matchedData<IProductTable>(req, { locations: ['body'] })
|
||||
await updateProductById(id, {
|
||||
...reqBody,
|
||||
buying_price: reqBody.buying_price || null,
|
||||
selling_price: reqBody.selling_price || null,
|
||||
stock: reqBody.stock || 0,
|
||||
product_category_id: reqBody.product_category_id || null,
|
||||
})
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Product data successfully updated'
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const deleteProductRoute = [
|
||||
param('id'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = matchedData(req)
|
||||
await deleteProductById(id)
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Product successfully deleted'
|
||||
}
|
||||
res.json(result)
|
||||
} catch (error) {
|
||||
next(createHttpError(500, error as Error))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const getProductByProductCodeRoute = [
|
||||
param('product_code'),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { product_code } = matchedData(req)
|
||||
const product = await getProductByProductCode(
|
||||
req.user!.id,
|
||||
product_code,
|
||||
)
|
||||
|
||||
if (!product) {
|
||||
return next(createHttpError(404, 'Produk tidak ditemukan'))
|
||||
}
|
||||
|
||||
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 }
|
|
@ -10,18 +10,19 @@ import parsePhoneNumberFromString from "libphonenumber-js"
|
|||
const addSupplier = [
|
||||
body('supplier_name').notEmpty().isString(),
|
||||
body('contact')
|
||||
.optional({ nullable: true })
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('address').optional().isString(),
|
||||
expressValidatorErrorHandler,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const reqData = matchedData<ISupplierTable>(req)
|
||||
const phone = parsePhoneNumberFromString(`+${reqData.contact}`);
|
||||
if (!phone?.isValid())
|
||||
next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
|
||||
if (!phone?.isValid() && !!reqData.contact)
|
||||
return next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
|
||||
try {
|
||||
const newSupplierId = await createSupplier({
|
||||
...reqData,
|
||||
contact: reqData.contact || null,
|
||||
user_id: req.user!.id
|
||||
})
|
||||
const result: TAPIResponse = {
|
||||
|
@ -78,7 +79,7 @@ const updateSupplierRoute = [
|
|||
param('id'),
|
||||
body('supplier_name').notEmpty().isString(),
|
||||
body('contact')
|
||||
.optional({ nullable: true })
|
||||
.optional({ values: 'falsy' })
|
||||
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
|
||||
body('address').optional().isString(),
|
||||
expressValidatorErrorHandler,
|
||||
|
@ -87,9 +88,12 @@ const updateSupplierRoute = [
|
|||
const { id } = matchedData(req, { locations: ['params'] })
|
||||
const reqBody = matchedData<ISupplierTable>(req, { locations: ['body'] })
|
||||
const phone = parsePhoneNumberFromString(`+${reqBody.contact}`);
|
||||
if (!phone?.isValid())
|
||||
next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
|
||||
await updateSupplier(id, reqBody)
|
||||
if (!phone?.isValid() && !!reqBody.contact)
|
||||
return next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
|
||||
await updateSupplier(id, {
|
||||
...reqBody,
|
||||
contact: reqBody.contact || null
|
||||
})
|
||||
const result: TAPIResponse = {
|
||||
success: true,
|
||||
message: 'Supplier data successfully updated'
|
||||
|
|
|
@ -4,7 +4,7 @@ import { body, param, matchedData } from "express-validator";
|
|||
import createHttpError from "http-errors";
|
||||
import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware
|
||||
import * as transactionsRepository from "../../repository/transaction"; // Repository untuk Transactions dalam bentuk mongoose
|
||||
import * as productsRepository from "../../repository/products"; // Untuk cek produk
|
||||
import * as productsRepository from "../../repository/productsRepository"; // Untuk cek produk
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { ITransaction } from "../../types/db-model";
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import os from 'os';
|
||||
|
||||
export function getLocalIP(): string {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const name in interfaces) {
|
||||
for (const iface of interfaces[name]!) {
|
||||
if (iface.family === "IPv4" && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "127.0.0.1";
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { IProductCategoryTable } from "../types/db-model";
|
||||
|
||||
export async function insertProductCategory(data: IProductCategoryTable) {
|
||||
const [id] = await db<IProductCategoryTable>('product_categories').insert(data)
|
||||
return id
|
||||
}
|
||||
|
||||
export function selectAllProductCategory(user_id: IProductCategoryTable['user_id']) {
|
||||
return db<IProductCategoryTable>('product_categories').where({ user_id })
|
||||
}
|
||||
|
||||
export function selectProductCategoryById(id: IProductCategoryTable['id']) {
|
||||
return db<IProductCategoryTable>('product_categories').where({ id })
|
||||
}
|
||||
|
||||
export function selectProductCategoryByName(category_name: IProductCategoryTable['category_name']) {
|
||||
return db<IProductCategoryTable>('product_categories').where({ category_name })
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
// repository/productsRepository.ts
|
||||
|
||||
import { db } from "../database/MySQL"; // Koneksi database menggunakan Knex
|
||||
import { IProduct } from "../types/db-model";
|
||||
|
||||
const TABLE_NAME = "products";
|
||||
|
||||
// Tambah produk baru
|
||||
export const createProduct = async (data: IProduct) => {
|
||||
try {
|
||||
const [product] = await db(TABLE_NAME).insert({
|
||||
product_name: data.product_name,
|
||||
price: data.price,
|
||||
product_category_id: data.product_category_id,
|
||||
}).returning("*");
|
||||
|
||||
return product;
|
||||
} catch (err) {
|
||||
throw new Error("Error creating product");
|
||||
}
|
||||
};
|
||||
|
||||
// Update produk by ID
|
||||
export const updateProductById = async (id: number, data: Partial<IProduct>) => {
|
||||
try {
|
||||
const [updatedProduct] = await db(TABLE_NAME)
|
||||
.where({ id })
|
||||
.update(data)
|
||||
.returning("*");
|
||||
|
||||
return updatedProduct;
|
||||
} catch (err) {
|
||||
throw new Error("Error updating product");
|
||||
}
|
||||
};
|
||||
|
||||
// Hapus produk by ID
|
||||
export const deleteProductById = async (id: number) => {
|
||||
try {
|
||||
const deleted = await db(TABLE_NAME).where({ id }).del();
|
||||
return deleted;
|
||||
} catch (err) {
|
||||
throw new Error("Error deleting product");
|
||||
}
|
||||
};
|
||||
|
||||
// Ambil produk by ID
|
||||
export const getProductById = async (id: number) => {
|
||||
try {
|
||||
const product = await db(TABLE_NAME).where({ id }).first();
|
||||
return product;
|
||||
} catch (err) {
|
||||
throw new Error("Error fetching product");
|
||||
}
|
||||
};
|
||||
|
||||
// Ambil semua produk
|
||||
export const getAllProducts = async () => {
|
||||
try {
|
||||
return await db(TABLE_NAME);
|
||||
} catch (err) {
|
||||
throw new Error("Error fetching all products");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import { db } from "../database/MySQL";
|
||||
import { IProductTable } from "../types/db-model";
|
||||
|
||||
export const insertProduct = async (data: IProductTable) => {
|
||||
const [id] = await db<IProductTable>('products').insert(data)
|
||||
return id
|
||||
}
|
||||
|
||||
export const selectProductsByUserId = (user_id: IProductTable['user_id']) => {
|
||||
return db<IProductTable>('products').where({ user_id })
|
||||
}
|
||||
|
||||
export const selectProductById = (id: IProductTable['id']) => {
|
||||
return db<IProductTable>('products').where({ id })
|
||||
}
|
||||
|
||||
export const getAllProducts = (user_id: number, limit: number, offset: number) => {
|
||||
return db<IProductTable>('products as p')
|
||||
.leftJoin('product_categories as c', 'p.product_category_id', 'c.id')
|
||||
.select('p.*', 'c.category_name')
|
||||
.where('p.user_id', user_id)
|
||||
.where('p.deleted', false)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
}
|
||||
|
||||
export const countProducts = async (id_user: number) => {
|
||||
const result = await db<IProductTable>('products')
|
||||
.where({ user_id: id_user, deleted: false })
|
||||
.count('id as count')
|
||||
.first<{
|
||||
count: string;
|
||||
}>();
|
||||
|
||||
return parseInt(result?.count || '0');
|
||||
};
|
||||
|
||||
export const selectProductByCategoryId = (product_category_id: IProductTable['product_category_id']) => {
|
||||
return db<IProductTable>('products').where({ product_category_id })
|
||||
}
|
||||
|
||||
export const countProductsByCategory = async (product_category_id: IProductTable['product_category_id']) => {
|
||||
const result = await db<IProductTable>('products')
|
||||
.where({ product_category_id })
|
||||
.count('id as count')
|
||||
.first<{
|
||||
count: string;
|
||||
}>();
|
||||
|
||||
return parseInt(result?.count || '0');
|
||||
};
|
||||
|
||||
export const selectProductByProductCode = (product_code: IProductTable['product_code'], user_id: IProductTable['user_id']) => {
|
||||
return db<IProductTable>('products').where({
|
||||
product_code,
|
||||
user_id,
|
||||
})
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
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);
|
||||
|
@ -10,6 +11,6 @@ router.patch('/forgot-password/:token', authRoute.forgotPasswordChangePassword);
|
|||
router.get('/verify/:token', authRoute.verify);
|
||||
router.post('/re-send-email-activation/:token', authRoute.resendEmailVerification);
|
||||
router.get('/refresh-token', authRoute.refreshToken);
|
||||
router.get('/logout', authRoute.logout);
|
||||
router.get('/logout', authenticate, authRoute.logout);
|
||||
|
||||
export default router
|
|
@ -3,15 +3,9 @@ var router = express.Router();
|
|||
|
||||
import authEndpoint from './auth'
|
||||
import supplierEndpoint from './supplier'
|
||||
import productCategoryEndpoint from './product_category'
|
||||
import productEndpoint from './product'
|
||||
import authenticate from '../../middleware/authMiddleware';
|
||||
// import productCategory from '../../controller/api/productCategoriesController';
|
||||
// import product from '../../controller/api/productsController';
|
||||
// import stockForecastingController from '../../controller/api/stockForecasting';
|
||||
// import stockPurchaseController from '../../controller/api/stockPurchases';
|
||||
// import supplierController from '../../controller/api/suppliers';
|
||||
// import transactionController from '../../controller/api/transactions';
|
||||
// import userFileController from '../../controller/api/userFiles';
|
||||
// const {jwtInfluencerMiddleware} = require("../middleware/authMiddleware");
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
res.send('Read docs.');
|
||||
|
@ -27,5 +21,7 @@ router.get('/is-authenticated', (req, res) => {
|
|||
})
|
||||
|
||||
router.use('/', supplierEndpoint)
|
||||
router.use('/', productEndpoint)
|
||||
router.use('/', productCategoryEndpoint)
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import express from 'express'
|
||||
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);
|
||||
|
||||
export default router
|
|
@ -0,0 +1,12 @@
|
|||
import express from 'express'
|
||||
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);
|
||||
|
||||
export default router
|
|
@ -1,11 +1,14 @@
|
|||
// src/server.ts
|
||||
import app from './app';
|
||||
import http from 'http';
|
||||
import os from 'os';
|
||||
import { getLocalIP } from './dev-core';
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const PORT = parseInt(process.env.PORT || "5000", 10);
|
||||
|
||||
const localIP = getLocalIP();
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🚀 Server running at http://localhost:${PORT}`);
|
||||
server.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`🚀 Server running at http://${localIP}:${PORT}`);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { insertProductCategory, selectAllProductCategory, selectProductCategoryById, selectProductCategoryByName } from "../repository/productCategoriesRepository";
|
||||
import { selectProductByCategoryId } from "../repository/productsRepository";
|
||||
import { IProductCategoryTable } from "../types/db-model";
|
||||
|
||||
export const createProductCategory = (data: IProductCategoryTable) => insertProductCategory(data)
|
||||
|
||||
export const showAllProductCategory = (user_id: IProductCategoryTable['user_id']) => selectAllProductCategory(user_id)
|
||||
|
||||
export const deleteProductCategory = async (id: IProductCategoryTable['id'], user_id: IProductCategoryTable['user_id']) => {
|
||||
const otherCategory = await selectProductCategoryByName('Other').first()
|
||||
let otherCategoryId = otherCategory?.id
|
||||
if (!otherCategoryId) {
|
||||
otherCategoryId = await createProductCategory({
|
||||
category_name: 'Other',
|
||||
user_id,
|
||||
} as IProductCategoryTable)
|
||||
}
|
||||
await selectProductByCategoryId(id).where({
|
||||
product_category_id: id
|
||||
}).update({
|
||||
product_category_id: otherCategoryId
|
||||
})
|
||||
return selectProductCategoryById(id).delete()
|
||||
}
|
||||
|
||||
export const updateProductCategoryById = async (id: IProductCategoryTable['id'], data: IProductCategoryTable) => selectProductCategoryById(id).update(data)
|
|
@ -0,0 +1,48 @@
|
|||
import { countProducts, getAllProducts, insertProduct, selectProductById, selectProductByProductCode, selectProductsByUserId } from "../repository/productsRepository"
|
||||
import { IProductTable } from "../types/db-model"
|
||||
|
||||
export const createProduct = async (data: IProductTable) => {
|
||||
const product = await selectProductByProductCode(data.product_code, data.user_id).first()
|
||||
if (product) {
|
||||
return await updateProductById(product.id, {
|
||||
deleted: false
|
||||
})
|
||||
} else {
|
||||
return await insertProduct(data)
|
||||
}
|
||||
}
|
||||
export const getProducts = async (
|
||||
id_user: number,
|
||||
page = 1,
|
||||
limit = 10
|
||||
) => {
|
||||
const safePage = Math.max(1, Number(page));
|
||||
const safeLimit = Math.max(1, Number(limit));
|
||||
const offset = (safePage - 1) * safeLimit;
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
getAllProducts(id_user, safeLimit, offset),
|
||||
countProducts(id_user)
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
page: safePage,
|
||||
limit: safeLimit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / safeLimit)
|
||||
}
|
||||
};
|
||||
};
|
||||
export const showProductById = async (id: IProductTable['id']) => selectProductById(id).first()
|
||||
export const updateProductById = async (id: IProductTable['id'], data: Partial<IProductTable>) => selectProductById(id).update(data)
|
||||
export const deleteProductById = async (id: IProductTable['id']) => {
|
||||
return selectProductById(id).update({
|
||||
deleted: true
|
||||
})
|
||||
}
|
||||
export const getProductByProductCode = (
|
||||
user_id: IProductTable['user_id'],
|
||||
product_code: IProductTable['product_code']
|
||||
) => selectProductByProductCode(product_code, user_id).first()
|
|
@ -13,22 +13,24 @@ export interface IProductCategoryTable {
|
|||
user_id: number;
|
||||
category_name: string;
|
||||
id: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface IProductTable {
|
||||
id: number;
|
||||
product_name: string;
|
||||
product_code: string;
|
||||
stock: number;
|
||||
price: number;
|
||||
buying_price: number | null;
|
||||
selling_price: number | null;
|
||||
product_category_id: number | null;
|
||||
user_id: number;
|
||||
product_category_id: number;
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export interface ISupplierTable {
|
||||
id: number;
|
||||
supplier_name: string;
|
||||
contact: string;
|
||||
contact: string | null;
|
||||
address: string;
|
||||
user_id: number;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue