first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.env
|
||||
seeders
|
||||
11
.sequelizerc
Normal file
11
.sequelizerc
Normal file
@@ -0,0 +1,11 @@
|
||||
const path = require("path");
|
||||
|
||||
require("ts-node/register");
|
||||
require("tsconfig-paths/register");
|
||||
|
||||
module.exports = {
|
||||
config: path.resolve("src/config/config.ts"),
|
||||
"models-path": path.resolve("src/models"),
|
||||
"migrations-path": path.resolve("src/migrations"),
|
||||
"seeders-path": path.resolve("src/seeders")
|
||||
};
|
||||
3
index.ts
Normal file
3
index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ServerApplication from "./src/server";
|
||||
|
||||
new ServerApplication();
|
||||
4784
package-lock.json
generated
Normal file
4784
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
package.json
Normal file
57
package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "ticketing-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Ticketing System API (Express + Sequelize + TypeScript)",
|
||||
"main": "index.ts",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"dev": "nodemon index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node index.ts",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"keywords": [
|
||||
"express",
|
||||
"sequelize",
|
||||
"typescript",
|
||||
"ticketing"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"auto-bind": "^4.0.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.6",
|
||||
"csurf": "^1.11.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"dotenv": "^17.4.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"helmet": "^8.1.0",
|
||||
"hpp": "^0.2.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"joi": "^18.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^2.1.1",
|
||||
"pg": "^8.20.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.8",
|
||||
"stimulsoft-reports-js": "^2026.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/csurf": "^1.11.5",
|
||||
"@types/exceljs": "^0.5.3",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/hpp": "^0.2.7",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^25.8.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
0
reports/tickets.mrt
Normal file
0
reports/tickets.mrt
Normal file
17
src/config/config.ts
Normal file
17
src/config/config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// sequelize.config.js
|
||||
module.exports = {
|
||||
development: {
|
||||
username: "postgres",
|
||||
password: "root",
|
||||
database: "ticketing-shomal",
|
||||
host: "127.0.0.1",
|
||||
dialect: "postgres",
|
||||
},
|
||||
production: {
|
||||
username: "postgres",
|
||||
password: "root",
|
||||
database: "ticketing-shomal-test",
|
||||
host: "127.0.0.1",
|
||||
dialect: "postgres",
|
||||
},
|
||||
};
|
||||
15
src/config/database.ts
Normal file
15
src/config/database.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { sequelize } from "../models";
|
||||
|
||||
async function initDB(): Promise<void> {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
|
||||
await sequelize.sync({ alter: true });
|
||||
console.log("✅ Database synced successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Database sync failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export default initDB;
|
||||
29
src/config/secure-app.ts
Normal file
29
src/config/secure-app.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import helmet from "helmet";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import csurf from "csurf";
|
||||
import hpp from "hpp";
|
||||
import { cors_option, helmet_option, limiter_option } from "../core/constants/server-configuration";
|
||||
|
||||
export const rateLimiter = rateLimit(limiter_option);
|
||||
|
||||
export const corsOptions = cors(cors_option);
|
||||
|
||||
export const securityHeaders = helmet(helmet_option);
|
||||
|
||||
export const csrfProtection = [
|
||||
cookieParser(),
|
||||
csurf({cookie: {httpOnly: true, secure: true, sameSite: "strict"}}),
|
||||
];
|
||||
|
||||
export const sanitizeData = [
|
||||
hpp(),
|
||||
];
|
||||
|
||||
export const secureApp = [
|
||||
corsOptions,
|
||||
rateLimiter,
|
||||
securityHeaders,
|
||||
...sanitizeData,
|
||||
];
|
||||
4
src/core/constants/index.ts
Normal file
4
src/core/constants/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const JWT_SECRET = process.env.JWT_SECRET || "secret";
|
||||
export const TOKEN_NAME = 'userToken'
|
||||
|
||||
export const requestType = ['software','hardware','his','rahkaran','taradod','general']
|
||||
90
src/core/constants/server-configuration.ts
Normal file
90
src/core/constants/server-configuration.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { CorsOptions } from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import { NextFunction } from "express";
|
||||
import { Options } from "express-rate-limit";
|
||||
import { HelmetOptions } from "helmet";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
import { Request } from "express";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
export const config = {
|
||||
port: process.env.PORT || 3500,
|
||||
db: {
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
},
|
||||
jwtSecret: process.env.JWT_SECRET || "secret",
|
||||
};
|
||||
|
||||
export const limiter_option: Partial<Options> = {
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 4000,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
|
||||
handler: (req: any, res: any, next: NextFunction) => {
|
||||
next(
|
||||
new createHttpError.TooManyRequests(
|
||||
"تعداد درخواست شما بیشتر از حد مجاز است ، در زمان دیگری مجدد درخواست دهید",
|
||||
),
|
||||
);
|
||||
},
|
||||
// message: {error: "Too many requests, please try again later."},
|
||||
};
|
||||
|
||||
export const helmet_option: HelmetOptions = {
|
||||
contentSecurityPolicy: {
|
||||
useDefaults: true,
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
// scriptSrc: ["'self'", "'unsafe-inline'", "https://trusted.cdn.com"],
|
||||
// styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
// imgSrc: ["'self'", "data:", "https:"],
|
||||
connectSrc: ["'self'"],
|
||||
// fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||
objectSrc: ["'none'"],
|
||||
upgradeInsecureRequests: [], // تبدیل اتومات http به https
|
||||
},
|
||||
},
|
||||
crossOriginEmbedderPolicy: true,
|
||||
crossOriginResourcePolicy: { policy: "same-origin" },
|
||||
frameguard: { action: "deny" }, // جلوگیری از Clickjacking
|
||||
referrerPolicy: { policy: "no-referrer" }, // جلوگیری از لو رفتن referrer
|
||||
xssFilter: true, // فعال کردن فیلتر XSS
|
||||
hsts: { maxAge: 63072000, includeSubDomains: true, preload: true }, // HSTS
|
||||
};
|
||||
|
||||
export const cors_option: CorsOptions = {
|
||||
origin: true,
|
||||
credentials: true,
|
||||
allowedHeaders: [
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
"x-upload-token", // 👈 اینو اضافه کن
|
||||
],
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
|
||||
maxAge: 600,
|
||||
// origin:["http://localhost:3000"]
|
||||
};
|
||||
|
||||
async function createDirectoryRoute(req: Request<any>) {
|
||||
const date = new Date();
|
||||
|
||||
const directory = path.join(__dirname, "..", "..", "..", "public", "images");
|
||||
req.body.fileUploadPath = path.join(directory, "original");
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
return directory;
|
||||
}
|
||||
|
||||
const storage = multer.memoryStorage(); // Keep files in memory (instead of disk)
|
||||
const uploadFile = multer({ storage: storage });
|
||||
|
||||
export { uploadFile };
|
||||
8
src/core/controller/main.controller.ts
Normal file
8
src/core/controller/main.controller.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import autoBind from "auto-bind";
|
||||
|
||||
export class Controller {
|
||||
constructor() {
|
||||
autoBind(this);
|
||||
}
|
||||
}
|
||||
|
||||
6
src/core/messages/index.ts
Normal file
6
src/core/messages/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const GlobalMessages = {
|
||||
success: {},
|
||||
errors: {
|
||||
server: "خطايي رخ داده است",
|
||||
},
|
||||
};
|
||||
29
src/core/middleware/auth.middleware.ts
Normal file
29
src/core/middleware/auth.middleware.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextFunction } from "express";
|
||||
import { ServerResponse } from "../types";
|
||||
import { JWT_SECRET, TOKEN_NAME } from "../constants";
|
||||
import createHttpError from "http-errors";
|
||||
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
const authMiddleware = (req:any, res:ServerResponse, next:NextFunction) => {
|
||||
// ۱. خواندن توکن از کوکیها
|
||||
const token = req.cookies[TOKEN_NAME]; // TOKEN_NAME همان متغیری است که در زمان ست کردن کوکی داشتید
|
||||
|
||||
if (!token) {
|
||||
throw new createHttpError.Unauthorized("لطفا وارد حساب كاربري خود شويد")
|
||||
}
|
||||
|
||||
try {
|
||||
// ۲. اعتبارسنجی توکن با استفاده از همان SECRET
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// ۳. قرار دادن اطلاعات کاربر در آبجکت درخواست برای استفاده در مسیرهای بعدی
|
||||
req.user = decoded;
|
||||
|
||||
next(); // ادامه به مسیر اصلی
|
||||
} catch (error) {
|
||||
throw new createHttpError.Forbidden("حساب كاربري شما نامعتبر است")
|
||||
}
|
||||
};
|
||||
|
||||
export default authMiddleware
|
||||
17
src/core/router/main.router.ts
Normal file
17
src/core/router/main.router.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import router from "express";
|
||||
import authRouter from "../../modules/auth/routes/auth.routes";
|
||||
import departmentRouter from "../../modules/department/routes/department.routes";
|
||||
import ticketRouter from "../../modules/ticket/routes/ticket.routes";
|
||||
import userRouter from "../../modules/user/routes/user.router";
|
||||
import reportRouter from "../../modules/report/routes/report.router";
|
||||
import authMiddleware from "../middleware/auth.middleware";
|
||||
|
||||
const mainRouter = router.Router();
|
||||
|
||||
mainRouter.use("/auth", authRouter);
|
||||
mainRouter.use("/department", authMiddleware, departmentRouter);
|
||||
mainRouter.use("/ticket", authMiddleware, ticketRouter);
|
||||
mainRouter.use("/user", authMiddleware, userRouter);
|
||||
mainRouter.use("/report", authMiddleware, reportRouter);
|
||||
|
||||
export default mainRouter;
|
||||
98
src/core/types/index.ts
Normal file
98
src/core/types/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Response } from "express";
|
||||
|
||||
export interface ServerResponseObject {
|
||||
status: number;
|
||||
data: any;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ServerErrorsObject {
|
||||
status: number;
|
||||
data?: any;
|
||||
error: {
|
||||
message: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type ServerResponse = Response<
|
||||
ServerResponseObject | ServerErrorsObject
|
||||
>;
|
||||
|
||||
export enum TicketPriority {
|
||||
LOW = "low",
|
||||
MEDIUM = "medium",
|
||||
HIGH = "high",
|
||||
CRITICAL = "critical",
|
||||
}
|
||||
export enum TicketStatus {
|
||||
OPEN = "open",
|
||||
PENDING = "pending",
|
||||
IN_PROGRESS = "in_progress",
|
||||
RESOLVED = "resolved",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
|
||||
export interface ticketRequestTypeDataType {
|
||||
id: string;
|
||||
|
||||
ticketNumber: string;
|
||||
|
||||
createdBy: string;
|
||||
|
||||
departmentId: string;
|
||||
|
||||
internalPhone?: string;
|
||||
|
||||
requestType: string;
|
||||
|
||||
priority: TicketPriority;
|
||||
|
||||
description: string;
|
||||
|
||||
relatedSystem?: string;
|
||||
|
||||
location?: string;
|
||||
|
||||
helpdeskAction?: string;
|
||||
|
||||
assignedTo?: string;
|
||||
|
||||
status: TicketStatus;
|
||||
|
||||
resolvedAt?: Date;
|
||||
|
||||
finalNotes?: string;
|
||||
|
||||
createdAt?: Date;
|
||||
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// reports
|
||||
|
||||
export interface TicketStats {
|
||||
total: number
|
||||
open: number
|
||||
inProgress: number
|
||||
resolved: number
|
||||
closed: number
|
||||
}
|
||||
|
||||
export interface DepartmentReport {
|
||||
departmentId: string
|
||||
departmentName: string
|
||||
totalTickets: number
|
||||
openTickets: number
|
||||
resolvedTickets: number
|
||||
}
|
||||
|
||||
export interface AgentReport {
|
||||
userId: string
|
||||
fullname: string
|
||||
totalAssigned: number
|
||||
resolved: number
|
||||
open: number
|
||||
}
|
||||
33
src/core/utils/functions.ts
Normal file
33
src/core/utils/functions.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Op, WhereOptions } from "sequelize";
|
||||
|
||||
export interface ReportFilters {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
priority?: string;
|
||||
departmentId?: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
export function buildTicketWhere(filters: ReportFilters): WhereOptions {
|
||||
const where: WhereOptions = {};
|
||||
|
||||
if (filters.startDate && filters.endDate) {
|
||||
where["createdAt"] = {
|
||||
[Op.between]: [new Date(filters.startDate), new Date(filters.endDate)],
|
||||
};
|
||||
}
|
||||
|
||||
if (filters.priority) {
|
||||
where["priority"] = filters.priority;
|
||||
}
|
||||
|
||||
if (filters.departmentId) {
|
||||
where["departmentId"] = filters.departmentId;
|
||||
}
|
||||
|
||||
if (filters.agentId) {
|
||||
where["assignedTo"] = filters.agentId;
|
||||
}
|
||||
|
||||
return where;
|
||||
}
|
||||
12
src/core/utils/generators.ts
Normal file
12
src/core/utils/generators.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const generateTicketNumber = (): string => {
|
||||
const now = new Date();
|
||||
|
||||
const date =
|
||||
now.getFullYear().toString() +
|
||||
String(now.getMonth() + 1).padStart(2, "0") +
|
||||
String(now.getDate()).padStart(2, "0");
|
||||
|
||||
const random = Math.floor(1000 + Math.random() * 9000);
|
||||
|
||||
return `TCK-${date}-${random}`;
|
||||
};
|
||||
22
src/migrations/20260518111632-create-tables.js
Normal file
22
src/migrations/20260518111632-create-tables.js
Normal file
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up (queryInterface, Sequelize) {
|
||||
/**
|
||||
* Add altering commands here.
|
||||
*
|
||||
* Example:
|
||||
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
|
||||
*/
|
||||
},
|
||||
|
||||
async down (queryInterface, Sequelize) {
|
||||
/**
|
||||
* Add reverting commands here.
|
||||
*
|
||||
* Example:
|
||||
* await queryInterface.dropTable('users');
|
||||
*/
|
||||
}
|
||||
};
|
||||
71
src/models/Department.ts
Normal file
71
src/models/Department.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Model,
|
||||
DataTypes,
|
||||
Optional,
|
||||
Sequelize
|
||||
} from "sequelize";
|
||||
|
||||
interface DepartmentAttributes {
|
||||
id: string;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
interface DepartmentCreationAttributes
|
||||
extends Optional<DepartmentAttributes, "id"> {}
|
||||
|
||||
class Department
|
||||
extends Model<DepartmentAttributes, DepartmentCreationAttributes>
|
||||
implements DepartmentAttributes
|
||||
{
|
||||
public id!: string;
|
||||
public slug!: string;
|
||||
public displayName!: string;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
static initModel(sequelize: Sequelize): typeof Department {
|
||||
Department.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
|
||||
slug: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
displayName: {
|
||||
type: DataTypes.STRING(30),
|
||||
allowNull: false,
|
||||
defaultValue: "تعريف نشده",
|
||||
field: "display_name",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "Department",
|
||||
tableName: "departments",
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
return Department;
|
||||
}
|
||||
|
||||
static associate(models: any) {
|
||||
Department.hasMany(models.Ticket, {
|
||||
foreignKey: "departmentId",
|
||||
as: "tickets",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Department;
|
||||
64
src/models/Permission.ts
Normal file
64
src/models/Permission.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Model,
|
||||
DataTypes,
|
||||
Optional,
|
||||
Sequelize
|
||||
} from "sequelize";
|
||||
|
||||
interface PermissionAttributes {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
interface PermissionCreationAttributes
|
||||
extends Optional<PermissionAttributes, "id"> {}
|
||||
|
||||
class Permission
|
||||
extends Model<PermissionAttributes, PermissionCreationAttributes>
|
||||
implements PermissionAttributes
|
||||
{
|
||||
public id!: string;
|
||||
public name!: string;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
static initModel(sequelize: Sequelize): typeof Permission {
|
||||
Permission.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "Permission",
|
||||
tableName: "permissions",
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
return Permission;
|
||||
}
|
||||
|
||||
static associate(models: any) {
|
||||
Permission.belongsToMany(models.Role, {
|
||||
through: models.RolePermission,
|
||||
foreignKey: "permissionId",
|
||||
otherKey: "roleId",
|
||||
as: "roles",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Permission;
|
||||
79
src/models/Role.ts
Normal file
79
src/models/Role.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Model,
|
||||
DataTypes,
|
||||
Optional,
|
||||
Sequelize
|
||||
} from "sequelize";
|
||||
|
||||
interface RoleAttributes {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
interface RoleCreationAttributes
|
||||
extends Optional<RoleAttributes, "id" | "description"> {}
|
||||
|
||||
class Role
|
||||
extends Model<RoleAttributes, RoleCreationAttributes>
|
||||
implements RoleAttributes
|
||||
{
|
||||
public id!: string;
|
||||
public name!: string;
|
||||
public description?: string;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
static initModel(sequelize: Sequelize): typeof Role {
|
||||
Role.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "Role",
|
||||
tableName: "roles",
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
return Role;
|
||||
}
|
||||
|
||||
static associate(models: any) {
|
||||
|
||||
// Role -> Users
|
||||
Role.hasMany(models.User, {
|
||||
foreignKey: "roleId",
|
||||
as: "users",
|
||||
});
|
||||
|
||||
// Role <-> Permission
|
||||
Role.belongsToMany(models.Permission, {
|
||||
through: models.RolePermission,
|
||||
foreignKey: "roleId",
|
||||
otherKey: "permissionId",
|
||||
as: "permissions",
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default Role;
|
||||
65
src/models/RolePermission.ts
Normal file
65
src/models/RolePermission.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Model,
|
||||
DataTypes,
|
||||
Sequelize
|
||||
} from "sequelize";
|
||||
|
||||
interface RolePermissionAttributes {
|
||||
roleId: string;
|
||||
permissionId: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
class RolePermission
|
||||
extends Model<RolePermissionAttributes>
|
||||
implements RolePermissionAttributes
|
||||
{
|
||||
public roleId!: string;
|
||||
public permissionId!: string;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
static initModel(sequelize: Sequelize): typeof RolePermission {
|
||||
RolePermission.init(
|
||||
{
|
||||
roleId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: "role_id",
|
||||
},
|
||||
|
||||
permissionId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: "permission_id",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "RolePermission",
|
||||
tableName: "role_permissions",
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
return RolePermission;
|
||||
}
|
||||
|
||||
static associate(models: any) {
|
||||
|
||||
RolePermission.belongsTo(models.Role, {
|
||||
foreignKey: "roleId",
|
||||
as: "role",
|
||||
});
|
||||
|
||||
RolePermission.belongsTo(models.Permission, {
|
||||
foreignKey: "permissionId",
|
||||
as: "permission",
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default RolePermission;
|
||||
167
src/models/Ticket.ts
Normal file
167
src/models/Ticket.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Model, DataTypes, Sequelize, Optional } from "sequelize";
|
||||
import { TicketPriority } from "../core/types";
|
||||
|
||||
interface TicketAttributes {
|
||||
id: string;
|
||||
ticketNumber: string;
|
||||
createdBy: string;
|
||||
departmentId: string;
|
||||
internalPhone?: string;
|
||||
requestType: string;
|
||||
priority: "low" | "medium" | "high" | "critical";
|
||||
description: string;
|
||||
relatedSystem?: string;
|
||||
location?: string;
|
||||
helpdeskAction?: string;
|
||||
assignedTo?: string;
|
||||
status: "open" | "pending" | "in_progress" | "resolved" | "closed";
|
||||
resolvedAt?: Date;
|
||||
finalNotes?: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
type TicketCreationAttributes = Optional<
|
||||
TicketAttributes,
|
||||
"id" | "priority" | "status" | "resolvedAt" | "finalNotes"
|
||||
>;
|
||||
|
||||
export class Ticket
|
||||
extends Model<TicketAttributes, TicketCreationAttributes>
|
||||
implements TicketAttributes
|
||||
{
|
||||
public id!: string;
|
||||
public ticketNumber!: string;
|
||||
public createdBy!: string;
|
||||
public departmentId!: string;
|
||||
public internalPhone?: string;
|
||||
public requestType!: string;
|
||||
public priority!: "low" | "medium" | "high" | "critical";
|
||||
public description!: string;
|
||||
public relatedSystem?: string;
|
||||
public location?: string;
|
||||
public helpdeskAction?: string;
|
||||
public assignedTo?: string;
|
||||
public status!: "open" | "pending" | "in_progress" | "resolved" | "closed";
|
||||
public resolvedAt?: Date;
|
||||
public finalNotes?: string;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
static initModel(sequelize: Sequelize): typeof Ticket {
|
||||
Ticket.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
|
||||
ticketNumber: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
field: "ticket_number",
|
||||
},
|
||||
|
||||
createdBy: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
field: "created_by",
|
||||
},
|
||||
|
||||
departmentId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: "department_id",
|
||||
},
|
||||
|
||||
internalPhone: {
|
||||
type: DataTypes.STRING(10),
|
||||
field: "internal_phone",
|
||||
},
|
||||
|
||||
requestType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
field: "request_type",
|
||||
},
|
||||
|
||||
priority: {
|
||||
type: DataTypes.ENUM(...Object.values(TicketPriority)),
|
||||
defaultValue: "medium",
|
||||
},
|
||||
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
relatedSystem: {
|
||||
type: DataTypes.STRING,
|
||||
field: "related_system",
|
||||
},
|
||||
|
||||
location: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
|
||||
helpdeskAction: {
|
||||
type: DataTypes.TEXT,
|
||||
field: "helpdesk_action",
|
||||
},
|
||||
|
||||
assignedTo: {
|
||||
type: DataTypes.UUID,
|
||||
field: "assigned_to",
|
||||
},
|
||||
|
||||
status: {
|
||||
type: DataTypes.ENUM(
|
||||
"open",
|
||||
"pending",
|
||||
"in_progress",
|
||||
"resolved",
|
||||
"closed",
|
||||
),
|
||||
defaultValue: "open",
|
||||
},
|
||||
|
||||
resolvedAt: {
|
||||
type: DataTypes.DATE,
|
||||
field: "resolved_at",
|
||||
},
|
||||
|
||||
finalNotes: {
|
||||
type: DataTypes.TEXT,
|
||||
field: "final_notes",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: "tickets",
|
||||
modelName: "Ticket",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
return Ticket;
|
||||
}
|
||||
|
||||
static associate(models: any) {
|
||||
|
||||
|
||||
Ticket.belongsTo(models.User, {
|
||||
foreignKey: "assignedTo",
|
||||
as: "assignee",
|
||||
});
|
||||
|
||||
Ticket.belongsTo(models.Department, {
|
||||
foreignKey: "departmentId",
|
||||
as: "department",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Ticket;
|
||||
95
src/models/User.ts
Normal file
95
src/models/User.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
Model,
|
||||
DataTypes,
|
||||
Optional,
|
||||
Sequelize
|
||||
} from "sequelize";
|
||||
|
||||
interface UserAttributes {
|
||||
id: string;
|
||||
fullname: string;
|
||||
nationalCode: string;
|
||||
mobile: string;
|
||||
roleId: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
interface UserCreationAttributes
|
||||
extends Optional<UserAttributes, "id"> {}
|
||||
|
||||
class User
|
||||
extends Model<UserAttributes, UserCreationAttributes>
|
||||
implements UserAttributes
|
||||
{
|
||||
public id!: string;
|
||||
public fullname!: string;
|
||||
public nationalCode!: string;
|
||||
public mobile!: string;
|
||||
public roleId!: string;
|
||||
|
||||
public readonly createdAt!: Date;
|
||||
public readonly updatedAt!: Date;
|
||||
|
||||
static initModel(sequelize: Sequelize): typeof User {
|
||||
User.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
|
||||
fullname: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
nationalCode: {
|
||||
type: DataTypes.STRING(30),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
field: "national_code",
|
||||
},
|
||||
|
||||
mobile: {
|
||||
type: DataTypes.STRING(11),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
roleId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: "role_id",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "User",
|
||||
tableName: "users",
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
return User;
|
||||
}
|
||||
|
||||
static associate(models: any) {
|
||||
|
||||
User.belongsTo(models.Role, {
|
||||
foreignKey: "roleId",
|
||||
as: "role",
|
||||
});
|
||||
|
||||
|
||||
|
||||
User.hasMany(models.Ticket, {
|
||||
foreignKey: "assignedTo",
|
||||
as: "assignedTickets",
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
31
src/models/index.ts
Normal file
31
src/models/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
|
||||
import Department from "./Department";
|
||||
import User from "./User";
|
||||
import Permission from "./Permission";
|
||||
import Role from "./Role";
|
||||
import RolePermission from "./RolePermission";
|
||||
import Ticket from "./Ticket";
|
||||
|
||||
const sequelize = new Sequelize("ticketing-shomal", "postgres", "root", {
|
||||
host: "127.0.0.1",
|
||||
dialect: "postgres",
|
||||
logging: false,
|
||||
});
|
||||
const models = {
|
||||
Department: Department.initModel(sequelize),
|
||||
User: User.initModel(sequelize),
|
||||
Role: Role.initModel(sequelize),
|
||||
Ticket: Ticket.initModel(sequelize),
|
||||
Permission: Permission.initModel(sequelize),
|
||||
RolePermission: RolePermission.initModel(sequelize),
|
||||
};
|
||||
|
||||
Object.values(models).forEach((model: any) => {
|
||||
if (typeof model.associate === "function") {
|
||||
model.associate(models);
|
||||
}
|
||||
});
|
||||
|
||||
export { sequelize };
|
||||
export default models;
|
||||
57
src/modules/auth/controller/auth.controller.ts
Normal file
57
src/modules/auth/controller/auth.controller.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import AuthService from "../services/auth.service";
|
||||
import { NextFunction } from "express";
|
||||
import { AuthValidationSchema } from "../validation/auth.validation";
|
||||
import { AuthMessages } from "../messages";
|
||||
import { ServerResponse } from "../../../core/types";
|
||||
import { TOKEN_NAME } from "../../../core/constants";
|
||||
import { GlobalMessages } from "../../../core/messages";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
|
||||
|
||||
class AuthControllerClass extends Controller {
|
||||
#service;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#service = AuthService;
|
||||
}
|
||||
|
||||
async login(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
await AuthValidationSchema.validateAsync(req.body || {});
|
||||
const token = await this.#service.login(
|
||||
req.body?.username,
|
||||
req?.body?.password,
|
||||
);
|
||||
res.cookie(TOKEN_NAME, token, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: "lax",
|
||||
maxAge: 24 * 60 * 60 * 1000,
|
||||
});
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data: {},
|
||||
message: AuthMessages.sucess.login,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async logout(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
res.clearCookie(TOKEN_NAME);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data: {},
|
||||
message: AuthMessages.sucess.logout,
|
||||
});
|
||||
} catch (error) {
|
||||
next(GlobalMessages.errors.server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AuthController = new AuthControllerClass();
|
||||
export default AuthController;
|
||||
14
src/modules/auth/messages/index.ts
Normal file
14
src/modules/auth/messages/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const AuthMessages ={
|
||||
sucess:{
|
||||
login:'با موفقيت وارد شديد',
|
||||
logout:"با موفقيت خارج شديد",
|
||||
edited:'با موفقيت ويرايش شد',
|
||||
done:'انجام شد'
|
||||
},
|
||||
error:{
|
||||
login:{
|
||||
incorrectData:'نام كاربري و يا رمز عبور اشتباه است',
|
||||
loginFailed:'خطا در ورود كاربر'
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/modules/auth/routes/auth.routes.ts
Normal file
10
src/modules/auth/routes/auth.routes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import express from 'express'
|
||||
import AuthController from '../controller/auth.controller';
|
||||
|
||||
const authRouter = express.Router();
|
||||
|
||||
|
||||
authRouter.post('/login',AuthController.login)
|
||||
authRouter.post('/logout',AuthController.logout)
|
||||
|
||||
export default authRouter;
|
||||
46
src/modules/auth/services/auth.service.ts
Normal file
46
src/modules/auth/services/auth.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import createHttpError from "http-errors";
|
||||
import { AuthMessages } from "../messages";
|
||||
import jwt from "jsonwebtoken";
|
||||
import dotenv from "dotenv";
|
||||
import User from "../../../models/User";
|
||||
import { JWT_SECRET } from "../../../core/constants";
|
||||
dotenv.config();
|
||||
|
||||
class AuthServiceClass {
|
||||
async login(username: string, password: string) {
|
||||
try {
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
nationalCode: username,
|
||||
mobile: password,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw createHttpError.Unauthorized(
|
||||
AuthMessages.error.login.incorrectData,
|
||||
);
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
roleId: user.roleId,
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: "1d" },
|
||||
);
|
||||
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw createHttpError.InternalServerError(
|
||||
AuthMessages.error.login.loginFailed,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AuthService = new AuthServiceClass();
|
||||
|
||||
export default AuthService;
|
||||
26
src/modules/auth/validation/auth.validation.ts
Normal file
26
src/modules/auth/validation/auth.validation.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Joi from "joi";
|
||||
|
||||
export const AuthValidationSchema = Joi.object({
|
||||
username: Joi.string()
|
||||
.length(10)
|
||||
.pattern(/^[0-9]+$/)
|
||||
.required()
|
||||
.messages({
|
||||
"string.base": "نام کاربری باید رشته باشد",
|
||||
"string.empty": "نام کاربری الزامی است",
|
||||
"string.length": "نام کاربری باید ۱۰ رقم باشد",
|
||||
"string.pattern.base": "نام کاربری باید کد ملی معتبر باشد",
|
||||
"any.required": "نام کاربری الزامی است",
|
||||
}),
|
||||
|
||||
password: Joi.string()
|
||||
.pattern(/^09[0-9]{9}$/)
|
||||
.required()
|
||||
.messages({
|
||||
"string.base": "رمز عبور باید رشته باشد",
|
||||
"string.empty": "رمز عبور الزامی است",
|
||||
"string.pattern.base": "رمز عبور باید شماره موبایل معتبر باشد",
|
||||
"any.required": "رمز عبور الزامی است",
|
||||
}),
|
||||
});
|
||||
|
||||
88
src/modules/department/controller/department.controller.ts
Normal file
88
src/modules/department/controller/department.controller.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import DepartmentService from "../services/department.service";
|
||||
import { NextFunction } from "express";
|
||||
import { DepartmentValidationSchema } from "../validation/department.validation";
|
||||
import { DepartmentMessages } from "../messages";
|
||||
import { ServerResponse } from "../../../core/types";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
|
||||
class DepartmentControllerClass extends Controller {
|
||||
#service;
|
||||
constructor() {
|
||||
super();
|
||||
this.#service = DepartmentService;
|
||||
}
|
||||
async getAll(req: any, res: ServerResponse, next: NextFunction) {
|
||||
|
||||
|
||||
try {
|
||||
const data = await this.#service.getAll();
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "Ok",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async getById(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const id = req?.params?.id;
|
||||
const data = await this.#service.getById(id);
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "Ok",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async create(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
await DepartmentValidationSchema.validateAsync(req.body || {});
|
||||
|
||||
await this.#service.create(req.body);
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data: {},
|
||||
message: DepartmentMessages.sucess.create,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async update(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const id = req?.params?.id;
|
||||
await this.#service.update(id, req?.body);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data: {},
|
||||
message: "",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async delete(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const id = req?.params?.id;
|
||||
|
||||
await this.#service.delete(id);
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data: {},
|
||||
message: DepartmentMessages.sucess.delete,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DepartmentController = new DepartmentControllerClass();
|
||||
|
||||
export default DepartmentController;
|
||||
16
src/modules/department/messages/index.ts
Normal file
16
src/modules/department/messages/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const DepartmentMessages = {
|
||||
sucess: {
|
||||
create: "با موفقيت ساخته شد",
|
||||
delete:"با موفقيت حذف گرديد",
|
||||
update:"با موفقيت به روزرساني گرديد"
|
||||
},
|
||||
error: {
|
||||
create: {
|
||||
unique: "اين واحد قبلا ثبت شده است",
|
||||
error:"خطا در انجام عمليات ثبت واحد"
|
||||
},
|
||||
delete:"خطا در حذف واحد",
|
||||
update:"خطا در به روزرساني واحد",
|
||||
notFound:"واحد يافت نشد"
|
||||
},
|
||||
};
|
||||
12
src/modules/department/routes/department.routes.ts
Normal file
12
src/modules/department/routes/department.routes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import router from "express";
|
||||
import DepartmentController from "../controller/department.controller";
|
||||
|
||||
const departmentRouter = router.Router();
|
||||
|
||||
departmentRouter.get("/all", DepartmentController.getAll);
|
||||
departmentRouter.get("/get/:id", DepartmentController.getById);
|
||||
departmentRouter.post("/create", DepartmentController.create);
|
||||
departmentRouter.delete("/remove/:id", DepartmentController.delete);
|
||||
departmentRouter.put("/update/:id", DepartmentController.update);
|
||||
|
||||
export default departmentRouter;
|
||||
105
src/modules/department/services/department.service.ts
Normal file
105
src/modules/department/services/department.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import createHttpError from "http-errors";
|
||||
import { UniqueConstraintError } from "sequelize";
|
||||
import { DepartmentMessages } from "../messages";
|
||||
import { GlobalMessages } from "../../../core/messages";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
import Department from "../../../models/Department";
|
||||
|
||||
interface CreateDepartmentDataBody {
|
||||
slug: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
class DepartmentServiceClass extends Controller {
|
||||
async getAll() {
|
||||
try {
|
||||
|
||||
const data = await Department.findAll({ raw: true });
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async getById(id: string) {
|
||||
try {
|
||||
const data = await Department.findOne({ where: { id } });
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async create(data: CreateDepartmentDataBody) {
|
||||
try {
|
||||
const { slug, displayName } = data;
|
||||
|
||||
await Department.create({
|
||||
slug,
|
||||
displayName,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error instanceof UniqueConstraintError) {
|
||||
throw new createHttpError.Conflict(
|
||||
DepartmentMessages.error.create.unique,
|
||||
);
|
||||
}
|
||||
|
||||
throw new createHttpError.InternalServerError(
|
||||
DepartmentMessages.error.create.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
async update(id: string, data: CreateDepartmentDataBody) {
|
||||
try {
|
||||
const { slug, displayName } = data;
|
||||
|
||||
const department = await Department.findByPk(id);
|
||||
|
||||
if (!department) {
|
||||
throw new createHttpError.NotFound(DepartmentMessages.error.notFound);
|
||||
}
|
||||
|
||||
if (slug && slug !== department.slug) {
|
||||
const exists = await Department.findOne({ where: { slug } });
|
||||
|
||||
if (exists) {
|
||||
throw new createHttpError.Conflict(
|
||||
DepartmentMessages.error.create.unique,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await department.update({
|
||||
slug,
|
||||
displayName,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new createHttpError.Conflict(DepartmentMessages.error.update);
|
||||
}
|
||||
}
|
||||
async delete(id: string) {
|
||||
try {
|
||||
const deleted = await Department.destroy({ where: { id } });
|
||||
if (!deleted) {
|
||||
throw new createHttpError.NotFound(DepartmentMessages.error.notFound);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
DepartmentMessages.error.delete,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DepartmentService = new DepartmentServiceClass();
|
||||
|
||||
export default DepartmentService;
|
||||
25
src/modules/department/validation/department.validation.ts
Normal file
25
src/modules/department/validation/department.validation.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Joi from "joi";
|
||||
|
||||
export const DepartmentValidationSchema = Joi.object({
|
||||
slug: Joi.string()
|
||||
.pattern(/^[a-z0-9-]+$/)
|
||||
.required()
|
||||
.messages({
|
||||
"string.base": "اسلاگ باید رشته باشد",
|
||||
"string.empty": "اسلاگ الزامی است",
|
||||
"string.pattern.base": "اسلاگ فقط میتواند شامل حروف انگلیسی کوچک، عدد و - باشد",
|
||||
"any.required": "اسلاگ الزامی است"
|
||||
}),
|
||||
|
||||
displayName: Joi.string()
|
||||
.min(2)
|
||||
.max(100)
|
||||
.required()
|
||||
.messages({
|
||||
"string.base": "نام نمایشی باید رشته باشد",
|
||||
"string.empty": "نام نمایشی الزامی است",
|
||||
"string.min": "نام نمایشی حداقل باید ۲ کاراکتر باشد",
|
||||
"string.max": "نام نمایشی حداکثر ۱۰۰ کاراکتر است",
|
||||
"any.required": "نام نمایشی الزامی است"
|
||||
})
|
||||
});
|
||||
213
src/modules/report/controller/report.controller.ts
Normal file
213
src/modules/report/controller/report.controller.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { NextFunction } from "express";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
import ReportService from "../service/report.service";
|
||||
import { ServerResponse } from "../../../core/types";
|
||||
|
||||
class ReportControllerClass extends Controller {
|
||||
#service;
|
||||
constructor() {
|
||||
super();
|
||||
this.#service = ReportService;
|
||||
}
|
||||
async ticketStats(req: any, res: ServerResponse, next: NextFunction) {
|
||||
const { start, end } = req.query;
|
||||
|
||||
try {
|
||||
const data = await this.#service.ticketStats(
|
||||
start ? new Date(start as string) : undefined,
|
||||
end ? new Date(end as string) : undefined,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async departmentReport(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.departmentReport();
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async agentPerformance(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.agentPerformance();
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async avgResolution(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.avgResolutionTime();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async criticalTickets(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.criticalTickets();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async kpi(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const system = await this.#service.systemScore();
|
||||
const operators = await this.#service.operatorScore();
|
||||
const departments = await this.#service.departmentScore();
|
||||
res.status(200).json({
|
||||
status: 200,
|
||||
data: { system, operators, departments },
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async prediction(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.monthlyTrend();
|
||||
console.log(data);
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async exportExcel(req: any, res: any, next: NextFunction) {
|
||||
const file = await this.#service.exportTickets();
|
||||
|
||||
res.setHeader(
|
||||
"Content-Type",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
);
|
||||
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=ticket-report.xlsx",
|
||||
);
|
||||
|
||||
try {
|
||||
return res.status(200).send(file);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async ticketsPerDay(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.ticketsPerDay();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async closureRate(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.closureRate();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async slaBreach(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.slaBreachReport();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async agentEfficiency(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.agentEfficiency();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async departmentLoad(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.departmentLoad();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async aging(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.agingReport();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "OK",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ReportController = new ReportControllerClass();
|
||||
|
||||
export default ReportController;
|
||||
61
src/modules/report/routes/report.router.ts
Normal file
61
src/modules/report/routes/report.router.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Router } from "express";
|
||||
import ReportController from "../controller/report.controller";
|
||||
|
||||
const reportRouter = Router();
|
||||
|
||||
reportRouter.get(
|
||||
"/stats",
|
||||
// requireAuth,
|
||||
// requirePermission("VIEW_REPORTS"),
|
||||
ReportController.ticketStats,
|
||||
);
|
||||
|
||||
reportRouter.get(
|
||||
"/departments",
|
||||
// requireAuth,
|
||||
// requirePermission("VIEW_REPORTS"),
|
||||
ReportController.departmentReport,
|
||||
);
|
||||
|
||||
reportRouter.get(
|
||||
"/agents",
|
||||
// requireAuth,
|
||||
// requirePermission("VIEW_REPORTS"),
|
||||
ReportController.agentPerformance,
|
||||
);
|
||||
|
||||
reportRouter.get(
|
||||
"/avg-resolution",
|
||||
// requireAuth,
|
||||
// requirePermission("VIEW_REPORTS"),
|
||||
ReportController.avgResolution,
|
||||
);
|
||||
|
||||
reportRouter.get(
|
||||
"/critical",
|
||||
// requireAuth,
|
||||
// requirePermission("VIEW_REPORTS"),
|
||||
ReportController.criticalTickets,
|
||||
);
|
||||
|
||||
reportRouter.get("/trend", ReportController.ticketsPerDay)
|
||||
|
||||
reportRouter.get("/closure-rate", ReportController.closureRate)
|
||||
|
||||
reportRouter.get("/sla", ReportController.slaBreach)
|
||||
|
||||
reportRouter.get("/aging", ReportController.aging)
|
||||
|
||||
reportRouter.get("/agent-efficiency", ReportController.agentEfficiency)
|
||||
|
||||
reportRouter.get("/department-load", ReportController.departmentLoad)
|
||||
|
||||
reportRouter.get("/kpi", ReportController.kpi)
|
||||
|
||||
reportRouter.get("/prediction", ReportController.prediction)
|
||||
|
||||
reportRouter.get("/export-excel", ReportController.exportExcel)
|
||||
|
||||
|
||||
|
||||
export default reportRouter;
|
||||
399
src/modules/report/service/report.service.ts
Normal file
399
src/modules/report/service/report.service.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
import { col, fn, literal, Op } from "sequelize";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
import Ticket from "../../../models/Ticket";
|
||||
import Department from "../../../models/Department";
|
||||
import User from "../../../models/User";
|
||||
import ExcelJS from "exceljs";
|
||||
|
||||
class ReportServiceClass extends Controller {
|
||||
// آمار کلی تیکت ها
|
||||
async ticketStats(start?: Date, end?: Date) {
|
||||
const where: any = {};
|
||||
|
||||
if (start && end) {
|
||||
where.createdAt = {
|
||||
[Op.between]: [start, end],
|
||||
};
|
||||
}
|
||||
|
||||
const total = await Ticket.count({ where });
|
||||
|
||||
const open = await Ticket.count({
|
||||
where: { ...where, status: "open" },
|
||||
});
|
||||
|
||||
const inProgress = await Ticket.count({
|
||||
where: { ...where, status: "in_progress" },
|
||||
});
|
||||
|
||||
const resolved = await Ticket.count({
|
||||
where: { ...where, status: "resolved" },
|
||||
});
|
||||
|
||||
const closed = await Ticket.count({
|
||||
where: { ...where, status: "closed" },
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
open,
|
||||
inProgress,
|
||||
resolved,
|
||||
closed,
|
||||
};
|
||||
}
|
||||
|
||||
// گزارش بر اساس دپارتمان
|
||||
async departmentReport() {
|
||||
const data = await Ticket.findAll({
|
||||
attributes: [
|
||||
"departmentId",
|
||||
[fn("COUNT", col("Ticket.id")), "totalTickets"],
|
||||
[
|
||||
fn("SUM", literal(`CASE WHEN status='open' THEN 1 ELSE 0 END`)),
|
||||
"openTickets",
|
||||
],
|
||||
[
|
||||
fn("SUM", literal(`CASE WHEN status='resolved' THEN 1 ELSE 0 END`)),
|
||||
"resolvedTickets",
|
||||
],
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: Department,
|
||||
as: "department",
|
||||
attributes: ["displayName"],
|
||||
},
|
||||
],
|
||||
group: ["departmentId", "department.id"],
|
||||
});
|
||||
|
||||
|
||||
console.log(data)
|
||||
return data;
|
||||
}
|
||||
|
||||
// گزارش عملکرد اپراتورها
|
||||
async agentPerformance() {
|
||||
const data = await Ticket.findAll({
|
||||
attributes: [
|
||||
"assignedTo",
|
||||
[fn("COUNT", col("Ticket.id")), "totalAssigned"],
|
||||
[
|
||||
fn("SUM", literal(`CASE WHEN status='resolved' THEN 1 ELSE 0 END`)),
|
||||
"resolved",
|
||||
],
|
||||
[
|
||||
fn("SUM", literal(`CASE WHEN status='open' THEN 1 ELSE 0 END`)),
|
||||
"open",
|
||||
],
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "assignee",
|
||||
attributes: ["id", "fullname"],
|
||||
},
|
||||
],
|
||||
group: ["assignedTo", "assignee.id"],
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// میانگین زمان حل تیکت
|
||||
async avgResolutionTime() {
|
||||
const data = await Ticket.findAll({
|
||||
attributes: [
|
||||
[
|
||||
fn(
|
||||
"AVG",
|
||||
literal(`EXTRACT(EPOCH FROM ("resolved_at" - "createdAt"))`),
|
||||
),
|
||||
"avgResolutionSeconds",
|
||||
],
|
||||
],
|
||||
// استفاده از literal برای رفع خطا و بهبود خوانایی
|
||||
where: literal(`"resolved_at" IS NOT NULL`) as any,
|
||||
});
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
// تیکت های بحرانی
|
||||
async criticalTickets() {
|
||||
return Ticket.findAll({
|
||||
where: {
|
||||
priority: "critical",
|
||||
status: {
|
||||
[Op.not]: "resolved",
|
||||
},
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Department,
|
||||
as: "department",
|
||||
attributes: ["displayName"],
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: "assignee",
|
||||
attributes: ["fullname"],
|
||||
},
|
||||
],
|
||||
order: [["createdAt", "DESC"]],
|
||||
});
|
||||
}
|
||||
|
||||
async ticketsPerDay(days: number = 30) {
|
||||
return Ticket.findAll({
|
||||
attributes: [
|
||||
[fn("DATE", col("createdAt")), "date"],
|
||||
[fn("COUNT", col("id")), "count"],
|
||||
],
|
||||
where: {
|
||||
createdAt: {
|
||||
[Op.gte]: literal(`NOW() - INTERVAL '${days} days'`),
|
||||
},
|
||||
},
|
||||
group: [fn("DATE", col("createdAt"))],
|
||||
order: [[fn("DATE", col("createdAt")), "ASC"]],
|
||||
});
|
||||
}
|
||||
|
||||
async closureRate() {
|
||||
const total = await Ticket.count();
|
||||
|
||||
const resolved = await Ticket.count({
|
||||
where: { status: "resolved" },
|
||||
});
|
||||
|
||||
const rate = total === 0 ? 0 : (resolved / total) * 100;
|
||||
|
||||
return {
|
||||
total,
|
||||
resolved,
|
||||
closureRate: rate.toFixed(2),
|
||||
};
|
||||
}
|
||||
|
||||
async slaBreachReport(slaHours: number = 48) {
|
||||
const breached = await Ticket.count({
|
||||
where: literal(`
|
||||
"resolved_at" IS NOT NULL AND
|
||||
EXTRACT(EPOCH FROM ("resolved_at" - "createdAt")) / 3600 > ${slaHours}
|
||||
`),
|
||||
});
|
||||
|
||||
const totalResolved = await Ticket.count({
|
||||
where: {
|
||||
resolvedAt: {
|
||||
[Op.ne]: null as any,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalResolved,
|
||||
breached,
|
||||
breachRate:
|
||||
totalResolved === 0 ? 0 : ((breached / totalResolved) * 100).toFixed(2),
|
||||
};
|
||||
}
|
||||
async agingReport() {
|
||||
return Ticket.findAll({
|
||||
attributes: [
|
||||
"id",
|
||||
"ticketNumber",
|
||||
[literal(`EXTRACT(DAY FROM (NOW() - "createdAt"))`), "ageInDays"],
|
||||
],
|
||||
where: {
|
||||
status: "open",
|
||||
},
|
||||
order: [[literal(`EXTRACT(DAY FROM (NOW() - "createdAt"))`), "DESC"]],
|
||||
});
|
||||
}
|
||||
|
||||
async agentEfficiency() {
|
||||
return Ticket.findAll({
|
||||
attributes: [
|
||||
"assignedTo",
|
||||
[fn("COUNT", col("Ticket.id")), "total"],
|
||||
[
|
||||
fn(
|
||||
"SUM",
|
||||
literal(`CASE WHEN "Ticket"."status"='resolved' THEN 1 ELSE 0 END`),
|
||||
),
|
||||
"resolved",
|
||||
],
|
||||
[
|
||||
fn(
|
||||
"AVG",
|
||||
literal(
|
||||
`EXTRACT(EPOCH FROM ("Ticket"."resolved_at" - "Ticket"."createdAt"))`,
|
||||
),
|
||||
),
|
||||
"avgResolutionSeconds",
|
||||
],
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "assignee",
|
||||
attributes: ["fullname"],
|
||||
},
|
||||
],
|
||||
group: ["Ticket.assigned_to", "assignee.id"],
|
||||
});
|
||||
}
|
||||
|
||||
async departmentLoad() {
|
||||
return Ticket.findAll({
|
||||
attributes: [
|
||||
"departmentId",
|
||||
[fn("COUNT", col("Ticket.id")), "totalTickets"],
|
||||
[
|
||||
fn("SUM", literal(`CASE WHEN status='open' THEN 1 ELSE 0 END`)),
|
||||
"openTickets",
|
||||
],
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: Department,
|
||||
as: "department",
|
||||
attributes: ["displayName"],
|
||||
},
|
||||
],
|
||||
group: ["departmentId", "department.id"],
|
||||
order: [[fn("COUNT", col("Ticket.id")), "DESC"]],
|
||||
});
|
||||
}
|
||||
async systemScore() {
|
||||
const total = await Ticket.count();
|
||||
|
||||
const resolved = await Ticket.count({
|
||||
where: { status: "resolved" },
|
||||
});
|
||||
|
||||
const open = await Ticket.count({
|
||||
where: { status: "open" },
|
||||
});
|
||||
|
||||
const resolutionRate = (resolved / total) * 100;
|
||||
|
||||
const score = resolutionRate * 0.7 + ((total - open) / total) * 30;
|
||||
|
||||
return {
|
||||
totalTickets: total,
|
||||
resolved,
|
||||
open,
|
||||
resolutionRate,
|
||||
score: Math.round(score),
|
||||
};
|
||||
}
|
||||
|
||||
async operatorScore() {
|
||||
return Ticket.findAll({
|
||||
attributes: [
|
||||
"assignedTo",
|
||||
[fn("COUNT", col("Ticket.id")), "total"],
|
||||
[fn("SUM", col("status = 'resolved'")), "resolved"],
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "assignee",
|
||||
attributes: ["fullname"],
|
||||
},
|
||||
],
|
||||
group: ["assignedTo", "assignee.id"],
|
||||
});
|
||||
}
|
||||
|
||||
async departmentScore() {
|
||||
return Ticket.findAll({
|
||||
attributes: ["departmentId", [fn("COUNT", col("Ticket.id")), "total"]],
|
||||
include: [
|
||||
{
|
||||
model: Department,
|
||||
as: "department",
|
||||
attributes: ["displayName"],
|
||||
},
|
||||
],
|
||||
group: ["departmentId", "department.id"],
|
||||
});
|
||||
}
|
||||
|
||||
async exportTickets() {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
|
||||
const ticketsSheet = workbook.addWorksheet("Tickets");
|
||||
const agentsSheet = workbook.addWorksheet("Agents");
|
||||
const deptSheet = workbook.addWorksheet("Departments");
|
||||
|
||||
const tickets = await Ticket.findAll({
|
||||
include: [
|
||||
{ model: Department, as: "department" },
|
||||
{ model: User, as: "assignee" },
|
||||
],
|
||||
});
|
||||
|
||||
ticketsSheet.columns = [
|
||||
{ header: "Ticket", key: "ticketNumber", width: 20 },
|
||||
{ header: "Priority", key: "priority", width: 15 },
|
||||
{ header: "Status", key: "status", width: 15 },
|
||||
{ header: "Department", key: "department", width: 20 },
|
||||
{ header: "Agent", key: "agent", width: 20 },
|
||||
{ header: "Created", key: "createdAt", width: 20 },
|
||||
];
|
||||
|
||||
tickets.forEach((t: any) => {
|
||||
ticketsSheet.addRow({
|
||||
ticketNumber: t.ticketNumber,
|
||||
priority: t.priority,
|
||||
status: t.status,
|
||||
department: t.department?.displayName,
|
||||
agent: t.assignee?.fullname,
|
||||
createdAt: t.createdAt,
|
||||
});
|
||||
});
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async monthlyTrend() {
|
||||
const data = await Ticket.findAll({
|
||||
attributes: [
|
||||
[fn("DATE_TRUNC", "month", col("createdAt")), "month"],
|
||||
[fn("COUNT", col("id")), "total"],
|
||||
],
|
||||
group: ["month"],
|
||||
order: [[col("month"), "ASC"]],
|
||||
});
|
||||
|
||||
const values = data.map((r: any) => parseInt(r.get("total")));
|
||||
|
||||
if (values.length < 2) {
|
||||
return { prediction: values[0] || 0 };
|
||||
}
|
||||
|
||||
const last = values[values.length - 1];
|
||||
const prev = values[values.length - 2];
|
||||
|
||||
const trend = last - prev;
|
||||
|
||||
const prediction = last + trend;
|
||||
|
||||
return {
|
||||
lastMonth: last,
|
||||
trend,
|
||||
predictedNextMonth: Math.max(prediction, 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ReportService = new ReportServiceClass();
|
||||
|
||||
export default ReportService;
|
||||
113
src/modules/ticket/controller/ticket.controller.ts
Normal file
113
src/modules/ticket/controller/ticket.controller.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { NextFunction } from "express";
|
||||
import { ServerResponse } from "../../../core/types";
|
||||
import TicketService from "../services/ticket.service";
|
||||
import { ticketValidationSchema } from "../validation/ticket.validation";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
|
||||
class TicketControllerClass extends Controller {
|
||||
#service;
|
||||
constructor() {
|
||||
super();
|
||||
this.#service = TicketService;
|
||||
}
|
||||
async getAllExport(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.getAll(req);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 201,
|
||||
data,
|
||||
message: "Ok",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async getAllReport(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.getAllReport(req, res);
|
||||
|
||||
// return res.status(200).json({
|
||||
// status: 201,
|
||||
// data,
|
||||
// message: "Ok",
|
||||
// });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async getAll(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.getAll(req);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 201,
|
||||
data,
|
||||
message: "Ok",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async getById(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const id = req?.params?.id;
|
||||
const data = await this.#service.getById(id);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "حذف شد",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async create(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
await ticketValidationSchema.validateAsync(req.body || {});
|
||||
await this.#service.create(req.body);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 201,
|
||||
data: {},
|
||||
message: "تيكت ثبت شد",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async update(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const id = req?.params?.id;
|
||||
await ticketValidationSchema.validateAsync(req.body || {});
|
||||
await this.#service.update(id, req.body);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 201,
|
||||
data: {},
|
||||
message: "تيكت ويرايش شد",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async remove(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const id = req?.params?.id;
|
||||
await this.#service.remove(id);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data: {},
|
||||
message: "تيكت حذف شد",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TicketController = new TicketControllerClass();
|
||||
|
||||
export default TicketController;
|
||||
14
src/modules/ticket/routes/ticket.routes.ts
Normal file
14
src/modules/ticket/routes/ticket.routes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import express from "express";
|
||||
import TicketController from "../controller/ticket.controller";
|
||||
|
||||
const ticketRouter = express.Router();
|
||||
|
||||
ticketRouter.post("/create", TicketController.create);
|
||||
ticketRouter.get("/all", TicketController.getAll);
|
||||
ticketRouter.get("/all/export", TicketController.getAllExport);
|
||||
ticketRouter.get("/all/report", TicketController.getAllReport);
|
||||
ticketRouter.get("/get/:id", TicketController.getById);
|
||||
ticketRouter.param("/update/:id", TicketController.update);
|
||||
ticketRouter.delete("/remove/:id", TicketController.remove);
|
||||
|
||||
export default ticketRouter;
|
||||
311
src/modules/ticket/services/ticket.service.ts
Normal file
311
src/modules/ticket/services/ticket.service.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import createHttpError from "http-errors";
|
||||
import { ticketRequestTypeDataType } from "../../../core/types";
|
||||
import { GlobalMessages } from "../../../core/messages";
|
||||
import Ticket from "../../../models/Ticket";
|
||||
import { generateTicketNumber } from "../../../core/utils/generators";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
import Department from "../../../models/Department";
|
||||
import User from "../../../models/User";
|
||||
import { Op } from "sequelize";
|
||||
import path from "node:path";
|
||||
const Stimulsoft = require("stimulsoft-reports-js");
|
||||
// برای اطمینان از دسترسی گلوبال
|
||||
const fs = require("fs");
|
||||
|
||||
class TicketServiceClass extends Controller {
|
||||
async getAll(req: any) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
departmentId,
|
||||
priority,
|
||||
status,
|
||||
user,
|
||||
requestType,
|
||||
startDate,
|
||||
endDate,
|
||||
} = req.query;
|
||||
|
||||
// ۱. محاسبه offset برای صفحهبندی
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// ۲. ساخت آبجکت فیلترها به صورت داینامیک
|
||||
const whereClause: any = {};
|
||||
if (departmentId) whereClause.departmentId = departmentId;
|
||||
if (priority) whereClause.priority = priority;
|
||||
if (status) whereClause.status = status;
|
||||
if (user) whereClause.assignedTo = user;
|
||||
if (requestType) whereClause.requestType = requestType;
|
||||
if (startDate || endDate) {
|
||||
whereClause.createdAt = {};
|
||||
if (startDate) {
|
||||
whereClause.createdAt[Op.gte] = new Date(startDate); // تاریخ شروع (بزرگتر مساوی)
|
||||
}
|
||||
if (endDate) {
|
||||
// برای اینکه شامل کلِ روز آخر باشد، میتوان یک روز به تاریخ پایان اضافه کرد
|
||||
// یا ساعت را ۲۳:۵۹:۵۹ تنظیم کرد
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
whereClause.createdAt[Op.lte] = end; // تاریخ پایان (کوچکتر مساوی)
|
||||
}
|
||||
}
|
||||
const { count, rows } = await Ticket.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{ model: Department, as: "department", attributes: ["displayName"] },
|
||||
{ model: User, as: "assignee", attributes: ["fullname"] },
|
||||
],
|
||||
order: [["createdAt", "DESC"]],
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset.toString()),
|
||||
});
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
totalItems: count,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
currentPage: parseInt(page),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async getAllReport(req: any, res: any) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
departmentId,
|
||||
priority,
|
||||
status,
|
||||
user,
|
||||
requestType,
|
||||
startDate,
|
||||
endDate,
|
||||
} = req.query;
|
||||
|
||||
// ۱. آمادهسازی مسیر و بارگذاری فایل گزارش
|
||||
const reportPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"reports",
|
||||
"tickets.mrt",
|
||||
);
|
||||
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
console.error("فایل گزارش پیدا نشد در مسیر:", reportPath);
|
||||
throw new createHttpError.NotFound("فایل گزارش یافت نشد.");
|
||||
}
|
||||
|
||||
const report = new Stimulsoft.Report.StiReport();
|
||||
report.loadFile(reportPath);
|
||||
|
||||
// ۲. فچ کردن دادهها از دیتابیس (مشابه قبل)
|
||||
const offset = (page - 1) * limit;
|
||||
const whereClause: any = {};
|
||||
if (departmentId) whereClause.departmentId = departmentId;
|
||||
if (priority) whereClause.priority = priority;
|
||||
if (status) whereClause.status = status;
|
||||
if (user) whereClause.assignedTo = user;
|
||||
if (requestType) whereClause.requestType = requestType;
|
||||
if (startDate || endDate) {
|
||||
whereClause.createdAt = {};
|
||||
if (startDate) whereClause.createdAt[Op.gte] = new Date(startDate);
|
||||
if (endDate) {
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
whereClause.createdAt[Op.lte] = end;
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await Ticket.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{ model: Department, as: "department", attributes: ["displayName"] },
|
||||
{ model: User, as: "assignee", attributes: ["fullname"] },
|
||||
],
|
||||
order: [["createdAt", "DESC"]],
|
||||
// برای گزارش، معمولاً کاربر میخواهد تمام دادههای فیلتر شده را ببیند، نه فقط 10 تا
|
||||
// اگر میخواهید گزارش فقط روی صفحه فعلی باشد، همین limit و offset را نگه دارید
|
||||
});
|
||||
|
||||
// ۳. تزریق داده به گزارش
|
||||
const dataSet = new Stimulsoft.System.Data.DataSet("TicketsData");
|
||||
// تبدیل دادههای Sequelize به فرمت ساده JSON
|
||||
const reportData = rows.map((r) => r.toJSON());
|
||||
dataSet.readJson({ tickets: reportData });
|
||||
|
||||
report.regData("TicketsData", "TicketsData", dataSet);
|
||||
report.dictionary.synchronize(); // بسیار مهم: دیتای جدید را به ساختار گزارش متصل میکند
|
||||
|
||||
// ۴. رندر و خروجی
|
||||
report.render();
|
||||
const pdfData = report.exportDocument(
|
||||
Stimulsoft.Report.StiExportFormat.Pdf,
|
||||
);
|
||||
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=tickets-report.pdf",
|
||||
);
|
||||
res.send(pdfData);
|
||||
} catch (error: any) {
|
||||
console.error("خطا در تولید گزارش:", error);
|
||||
// اگر از قبل خطا را هندل کردیم، همان را پاس میدهیم
|
||||
if (error.status) throw error;
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async getAllExport(req: any) {
|
||||
try {
|
||||
const {
|
||||
departmentId,
|
||||
priority,
|
||||
status,
|
||||
user,
|
||||
requestType,
|
||||
startDate,
|
||||
endDate,
|
||||
} = req.query;
|
||||
|
||||
// ۲. ساخت آبجکت فیلترها به صورت داینامیک
|
||||
const whereClause: any = {};
|
||||
if (departmentId) whereClause.departmentId = departmentId;
|
||||
if (priority) whereClause.priority = priority;
|
||||
if (status) whereClause.status = status;
|
||||
if (user) whereClause.assignedTo = user;
|
||||
if (requestType) whereClause.requestType = requestType;
|
||||
if (startDate || endDate) {
|
||||
whereClause.createdAt = {};
|
||||
if (startDate) {
|
||||
whereClause.createdAt[Op.gte] = new Date(startDate); // تاریخ شروع (بزرگتر مساوی)
|
||||
}
|
||||
if (endDate) {
|
||||
// برای اینکه شامل کلِ روز آخر باشد، میتوان یک روز به تاریخ پایان اضافه کرد
|
||||
// یا ساعت را ۲۳:۵۹:۵۹ تنظیم کرد
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
whereClause.createdAt[Op.lte] = end; // تاریخ پایان (کوچکتر مساوی)
|
||||
}
|
||||
}
|
||||
const { count, rows } = await Ticket.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{ model: Department, as: "department", attributes: ["displayName"] },
|
||||
{ model: User, as: "assignee", attributes: ["fullname"] },
|
||||
],
|
||||
order: [["createdAt", "DESC"]],
|
||||
});
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async getById(id: string) {
|
||||
try {
|
||||
const data = await Ticket.findOne({
|
||||
where: { ticketNumber: id },
|
||||
include: [
|
||||
{ model: Department, as: "department", attributes: ["displayName"] },
|
||||
{ model: User, as: "assignee", attributes: ["fullname"] },
|
||||
],
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async create(data: ticketRequestTypeDataType) {
|
||||
try {
|
||||
if (data.status === "resolved") {
|
||||
data.resolvedAt = new Date();
|
||||
}
|
||||
await Ticket.create({
|
||||
ticketNumber: generateTicketNumber(),
|
||||
departmentId: data.departmentId,
|
||||
description: data.description,
|
||||
requestType: data.requestType,
|
||||
assignedTo: data.assignedTo,
|
||||
finalNotes: data.finalNotes,
|
||||
createdBy: data.createdBy,
|
||||
internalPhone: data.internalPhone,
|
||||
helpdeskAction: data.helpdeskAction,
|
||||
location: data.location,
|
||||
priority: data.priority,
|
||||
relatedSystem: data.relatedSystem,
|
||||
resolvedAt: data.resolvedAt,
|
||||
status: data.status,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async update(id: string, data: Partial<ticketRequestTypeDataType>) {
|
||||
try {
|
||||
const ticket = await Ticket.findByPk(id);
|
||||
|
||||
if (!ticket) {
|
||||
throw new createHttpError.NotFound("تيكت يافت نشد");
|
||||
}
|
||||
|
||||
if (data.status === "resolved" && !ticket.resolvedAt) {
|
||||
data.resolvedAt = new Date();
|
||||
}
|
||||
|
||||
await ticket.update(data);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
async remove(id: string) {
|
||||
try {
|
||||
const ticket = await Ticket.findByPk(id);
|
||||
|
||||
if (!ticket) {
|
||||
throw new createHttpError.NotFound("تيكت يافت نشد");
|
||||
}
|
||||
|
||||
if (ticket.status === "open") {
|
||||
throw new createHttpError.NotAcceptable(
|
||||
"امكان حذف تيكت باز وجود ندارد",
|
||||
);
|
||||
}
|
||||
await ticket.destroy();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TicketService = new TicketServiceClass();
|
||||
|
||||
export default TicketService;
|
||||
71
src/modules/ticket/validation/ticket.validation.ts
Normal file
71
src/modules/ticket/validation/ticket.validation.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import Joi from "joi";
|
||||
import { TicketPriority, TicketStatus } from "../../../core/types";
|
||||
|
||||
export const ticketValidationSchema = Joi.object({
|
||||
// ticketNumber: Joi.string().optional().allow(null,""),
|
||||
departmentId: Joi.string().uuid().required().messages({
|
||||
"string.base": "شناسه دپارتمان نامعتبر است",
|
||||
"string.empty": "شناسه دپارتمان الزامی است",
|
||||
"string.guid": "فرمت شناسه دپارتمان صحیح نیست",
|
||||
"any.required": "شناسه دپارتمان الزامی است",
|
||||
}),
|
||||
|
||||
internalPhone: Joi.string()
|
||||
.pattern(/^[0-9]{3,10}$/)
|
||||
.optional()
|
||||
.messages({
|
||||
"string.pattern.base": "شماره داخلی باید فقط عدد و بین ۳ تا ۱۰ رقم باشد",
|
||||
}),
|
||||
|
||||
requestType: Joi.string().required().messages({
|
||||
"string.empty": "نوع درخواست الزامی است",
|
||||
"any.required": "نوع درخواست الزامی است",
|
||||
}),
|
||||
|
||||
priority: Joi.string()
|
||||
.valid(...Object.values(TicketPriority))
|
||||
.optional()
|
||||
.messages({
|
||||
"any.only": "اولویت انتخاب شده معتبر نیست",
|
||||
}),
|
||||
|
||||
description: Joi.string().min(5).required().messages({
|
||||
"string.empty": "توضیحات الزامی است",
|
||||
"string.min": "توضیحات باید حداقل ۵ کاراکتر باشد",
|
||||
"any.required": "توضیحات الزامی است",
|
||||
}),
|
||||
|
||||
relatedSystem: Joi.string().optional().allow("").messages({
|
||||
"string.base": "سیستم مرتبط نامعتبر است",
|
||||
}),
|
||||
|
||||
location: Joi.string().optional().allow("").messages({
|
||||
"string.base": "موقعیت وارد شده نامعتبر است",
|
||||
}),
|
||||
createdBy: Joi.string().required().allow("").messages({
|
||||
"string.empty": "نام نام خانوادگي كاربر الزامی است",
|
||||
"any.required": "نام نام خانوادگي كاربر الزامی است",
|
||||
}),
|
||||
helpdeskAction: Joi.string().optional().allow("").messages({
|
||||
"string.base": "اقدام واحد پشتیبانی نامعتبر است",
|
||||
}),
|
||||
|
||||
assignedTo: Joi.string().uuid().optional().messages({
|
||||
"string.guid": "شناسه کاربر تخصیص داده شده نامعتبر است",
|
||||
}),
|
||||
|
||||
status: Joi.string()
|
||||
.valid(...Object.values(TicketStatus))
|
||||
.optional()
|
||||
.messages({
|
||||
"any.only": "وضعیت تیکت نامعتبر است",
|
||||
}),
|
||||
|
||||
resolvedAt: Joi.date().optional().messages({
|
||||
"date.base": "تاریخ حل شدن نامعتبر است",
|
||||
}),
|
||||
|
||||
finalNotes: Joi.string().optional().allow("").messages({
|
||||
"string.base": "یادداشت نهایی نامعتبر است",
|
||||
}),
|
||||
});
|
||||
30
src/modules/user/controller/user.controller.ts
Normal file
30
src/modules/user/controller/user.controller.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { NextFunction } from "express";
|
||||
import { ServerResponse } from "../../../core/types";
|
||||
import { Controller } from "../../../core/controller/main.controller";
|
||||
import UserService from "../services/user.service";
|
||||
|
||||
class UserControllerClass extends Controller {
|
||||
#service;
|
||||
constructor() {
|
||||
super();
|
||||
this.#service = UserService;
|
||||
}
|
||||
async getAll(req: any, res: ServerResponse, next: NextFunction) {
|
||||
try {
|
||||
const data = await this.#service.getAll();
|
||||
|
||||
return res.status(200).json({
|
||||
status: 200,
|
||||
data,
|
||||
message: "Ok",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const UserController = new UserControllerClass();
|
||||
|
||||
export default UserController;
|
||||
8
src/modules/user/routes/user.router.ts
Normal file
8
src/modules/user/routes/user.router.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import router from "express";
|
||||
import UserController from "../controller/user.controller";
|
||||
|
||||
const userRouter = router.Router();
|
||||
|
||||
userRouter.get("/all", UserController.getAll);
|
||||
|
||||
export default userRouter;
|
||||
20
src/modules/user/services/user.service.ts
Normal file
20
src/modules/user/services/user.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import createHttpError from "http-errors";
|
||||
import { GlobalMessages } from "../../../core/messages";
|
||||
import User from "../../../models/User";
|
||||
|
||||
class UserServiceClass {
|
||||
async getAll() {
|
||||
try {
|
||||
const data = await User.findAll({ raw: true });
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new createHttpError.InternalServerError(
|
||||
GlobalMessages.errors.server,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const UserService = new UserServiceClass();
|
||||
|
||||
export default UserService;
|
||||
91
src/server.ts
Normal file
91
src/server.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import path from "node:path";
|
||||
import initDB from "./config/database";
|
||||
import cookieParser from "cookie-parser";
|
||||
import createHttpError from "http-errors";
|
||||
import { ServerErrorsObject, ServerResponse } from "./core/types";
|
||||
import { secureApp } from "./config/secure-app";
|
||||
import mainRouter from "./core/router/main.router";
|
||||
import dotenv from "dotenv";
|
||||
import { seedDepartments } from "./seeders/department.seed";
|
||||
import { seedUsers } from "./seeders/user.seed";
|
||||
import { seedRoles } from "./seeders/role.seed";
|
||||
const express = require("express") as typeof import("express");
|
||||
dotenv.config();
|
||||
|
||||
export default class ServerApplication {
|
||||
#PORT = process.env.PORT || 8000;
|
||||
#APP = express();
|
||||
constructor() {
|
||||
this.serverConfiguration();
|
||||
this.StartApplication();
|
||||
this.InitClientSession();
|
||||
this.RoutesConfiguration();
|
||||
this.ErrorHandlingConfiguration();
|
||||
}
|
||||
|
||||
async serverConfiguration() {
|
||||
this.#APP.use(secureApp);
|
||||
this.#APP.use(express.json());
|
||||
this.#APP.use(express.urlencoded({ extended: true }));
|
||||
|
||||
this.#APP.set("json spaces", 2);
|
||||
this.#APP.use(
|
||||
"/media/images",
|
||||
express.static(path.join(__dirname, "..", "media", "images")),
|
||||
);
|
||||
this.#APP.use(
|
||||
"/media/videos",
|
||||
express.static(path.join(__dirname, "..", "media", "videos")),
|
||||
);
|
||||
}
|
||||
async StartApplication() {
|
||||
await initDB();
|
||||
// await seedDepartments();
|
||||
// await seedRoles();
|
||||
|
||||
// await seedUsers();
|
||||
|
||||
this.#APP.listen(this.#PORT, () => {
|
||||
console.log(
|
||||
`Server Running on PORT ${this.#PORT} url : ${"http://localhost:"}${
|
||||
this.#PORT
|
||||
}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
InitClientSession() {
|
||||
this.#APP.use(cookieParser(process.env.COOKIE_PARSER_SECRET_KEY));
|
||||
}
|
||||
RoutesConfiguration() {
|
||||
this.#APP.get("/", (req, res) => res.send(""));
|
||||
this.#APP.use("/api/v1", mainRouter);
|
||||
}
|
||||
ErrorHandlingConfiguration() {
|
||||
this.#APP.use((req: any, res: Response, next: NextFunction) => {
|
||||
next(createHttpError.NotFound("این آدرس یافت نشد"));
|
||||
});
|
||||
this.#APP.use(
|
||||
async (error: any, req: any, res: ServerResponse, next: NextFunction) => {
|
||||
// await ErrorLog.create({
|
||||
// message: error.message,
|
||||
// stack: error.stack,
|
||||
// severity: "HIGH",
|
||||
// });
|
||||
// console.log(error);
|
||||
const serverError = createHttpError.InternalServerError();
|
||||
const statusCode = error.status || serverError.status;
|
||||
const message: string = error.message || serverError.message;
|
||||
|
||||
const errorObject: ServerErrorsObject = {
|
||||
status: statusCode,
|
||||
error: {
|
||||
message,
|
||||
},
|
||||
};
|
||||
|
||||
return res.status(statusCode).json(errorObject);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
||||
|
||||
"outDir": "dist"
|
||||
},
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user