From df1dda764cb8a973b36376ff51663afc5ae63bcd Mon Sep 17 00:00:00 2001 From: webserver-lab Date: Wed, 27 May 2026 22:50:29 +0330 Subject: [PATCH] added modules & forms jobcaegory-jobrequest-referral-relation etc --- .gitignore | 1 + package-lock.json | 5 + src/core/router/main.router.ts | 8 +- .../controller/computerSkill.controller.ts | 76 ++++++++++ .../routes/computerSkill.routes.ts | 15 ++ .../service/computerSkill.service.ts | 66 +++++++++ .../controller/courseForm.controller.ts | 42 ++++-- .../course/service/courseForm.service.ts | 51 ++++--- .../controller/educationForm.controller.ts | 21 +++ .../service/educationForm.service.ts | 64 ++++++--- src/modules/forms/index.routes.ts | 14 +- .../controller/jobRequest.controller.ts | 76 ++++++++++ .../jobRequest/routes/jobRequest.routes.ts | 16 +++ .../jobRequest/service/jobRequest.service.ts | 53 +++++++ .../controller/languageSkill.controller.ts | 98 +++++++++++++ .../routes/languageSkill.routes.ts | 12 ++ .../service/langugeSkill.service.ts | 69 +++++++++ .../controller/referral.controller.ts | 94 +++++++++++++ .../forms/referral/routes/referral.routes.ts | 16 +++ .../referral/service/referral.service.ts | 41 ++++++ .../controller/relation.controller.ts | 81 +++++++++++ .../forms/relation/routes/relation.routes.ts | 16 +++ .../relation/service/relation.service.ts | 79 +++++++++++ .../controller/workExperience.controller.ts | 130 +++++++++++++++++ .../routes/workExperience.routes.ts | 17 +++ .../service/workExperience.service.ts | 119 ++++++++++++++++ .../controller/jobCategory.controller.ts | 90 ++++++++++++ .../job-category/routes/jobCategory.routes.ts | 12 ++ .../service/jobCategory.service.ts | 45 ++++++ .../user/controller/user.controller.ts | 93 ++++++++++++ src/modules/user/routes/user.routes.ts | 18 +++ src/modules/user/service/user.service.ts | 132 ++++++++++++++++++ 32 files changed, 1624 insertions(+), 46 deletions(-) create mode 100644 .gitignore create mode 100644 src/modules/forms/computerSkill/controller/computerSkill.controller.ts create mode 100644 src/modules/forms/computerSkill/routes/computerSkill.routes.ts create mode 100644 src/modules/forms/computerSkill/service/computerSkill.service.ts create mode 100644 src/modules/forms/jobRequest/controller/jobRequest.controller.ts create mode 100644 src/modules/forms/jobRequest/routes/jobRequest.routes.ts create mode 100644 src/modules/forms/jobRequest/service/jobRequest.service.ts create mode 100644 src/modules/forms/languageSkill/controller/languageSkill.controller.ts create mode 100644 src/modules/forms/languageSkill/routes/languageSkill.routes.ts create mode 100644 src/modules/forms/languageSkill/service/langugeSkill.service.ts create mode 100644 src/modules/forms/referral/controller/referral.controller.ts create mode 100644 src/modules/forms/referral/routes/referral.routes.ts create mode 100644 src/modules/forms/referral/service/referral.service.ts create mode 100644 src/modules/forms/relation/controller/relation.controller.ts create mode 100644 src/modules/forms/relation/routes/relation.routes.ts create mode 100644 src/modules/forms/relation/service/relation.service.ts create mode 100644 src/modules/forms/workExperience/controller/workExperience.controller.ts create mode 100644 src/modules/forms/workExperience/routes/workExperience.routes.ts create mode 100644 src/modules/forms/workExperience/service/workExperience.service.ts create mode 100644 src/modules/job-category/controller/jobCategory.controller.ts create mode 100644 src/modules/job-category/routes/jobCategory.routes.ts create mode 100644 src/modules/job-category/service/jobCategory.service.ts create mode 100644 src/modules/user/controller/user.controller.ts create mode 100644 src/modules/user/routes/user.routes.ts create mode 100644 src/modules/user/service/user.service.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6647e23..87f6c76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -211,6 +211,7 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -269,6 +270,7 @@ "resolved": "https://package-mirror.liara.ir/repository/npm/@types/node/-/node-25.9.0.tgz", "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -1172,6 +1174,7 @@ "resolved": "https://package-mirror.liara.ir/repository/npm/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -2245,6 +2248,7 @@ "resolved": "https://package-mirror.liara.ir/repository/npm/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -3148,6 +3152,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/core/router/main.router.ts b/src/core/router/main.router.ts index ce12285..00a0b6d 100644 --- a/src/core/router/main.router.ts +++ b/src/core/router/main.router.ts @@ -1,12 +1,12 @@ import { Router } from 'express'; import AuthRouter from '../../modules/auth/router/auth.routes'; +import formRouter from '../../modules/forms/index.routes'; +import userRouter from '../../modules/user/routes/user.routes'; const mainRouter = Router(); -// mainRouter.use('/user',userRouter) -// mainRouter.use('/applicant',applicantRouter) mainRouter.use('/auth',AuthRouter) -// mainRouter.use('/user',userRouter) - +mainRouter.use('/user',userRouter) +mainRouter.use('/form',formRouter) export default mainRouter; \ No newline at end of file diff --git a/src/modules/forms/computerSkill/controller/computerSkill.controller.ts b/src/modules/forms/computerSkill/controller/computerSkill.controller.ts new file mode 100644 index 0000000..04010d0 --- /dev/null +++ b/src/modules/forms/computerSkill/controller/computerSkill.controller.ts @@ -0,0 +1,76 @@ +// modules/.../computer-skill/controller/computerSkill.controller.ts +import { NextFunction } from "express"; +import { Controller } from "../../../../core/controller/main.controller"; +import { ServerResponse } from "../../../../core/types"; +import ComputerSkillService from "../service/computerSkill.service"; + +class ComputerSkillControllerClass extends Controller { + #service; + + constructor() { + super(); + this.#service = ComputerSkillService; + } + + // (اختیاری) برای ادمین + 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) { + next(error); + } + } + + // دریافت رکورد مهارت‌های کاربر جاری + async getMine(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const data = await this.#service.getMine(applicantId); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + + // ایجاد یا بروزرسانی رکورد مهارت‌های کاربر جاری + async upsertMine(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + + const data = await this.#service.upsertMine(applicantId, req.body); + + return res.status(200).json({ + status: 200, + data, + message: "ذخیره شد", + }); + } catch (error) { + next(error); + } + } + + // حذف رکورد مهارت‌های کاربر جاری + async removeMine(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + await this.#service.removeMine(applicantId); + + return res.status(200).json({ + status: 200, + data: {}, + message: "حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const ComputerSkillController = new ComputerSkillControllerClass(); +export default ComputerSkillController; diff --git a/src/modules/forms/computerSkill/routes/computerSkill.routes.ts b/src/modules/forms/computerSkill/routes/computerSkill.routes.ts new file mode 100644 index 0000000..5cca241 --- /dev/null +++ b/src/modules/forms/computerSkill/routes/computerSkill.routes.ts @@ -0,0 +1,15 @@ +// modules/.../computer-skill/routes/computerSkill.routes.ts +import { Router } from "express"; +import ComputerSkillController from "../controller/computerSkill.controller"; + +const computerSkillRouter = Router(); + +// پیشنهاد: این روت‌ها پشت authMiddleware باشند +computerSkillRouter.get("/me", ComputerSkillController.getMine.bind(ComputerSkillController)); +computerSkillRouter.put("/me", ComputerSkillController.upsertMine.bind(ComputerSkillController)); +computerSkillRouter.delete("/me", ComputerSkillController.removeMine.bind(ComputerSkillController)); + +// (اختیاری) فقط برای ادمین: +// computerSkillRouter.get("/", ComputerSkillController.getAll.bind(ComputerSkillController)); + +export default computerSkillRouter; diff --git a/src/modules/forms/computerSkill/service/computerSkill.service.ts b/src/modules/forms/computerSkill/service/computerSkill.service.ts new file mode 100644 index 0000000..8aa75d9 --- /dev/null +++ b/src/modules/forms/computerSkill/service/computerSkill.service.ts @@ -0,0 +1,66 @@ +// modules/.../computer-skill/service/computerSkill.service.ts + +import { ComputerSkill } from "../../../../models/ComputerSkill"; + +const SKILL_LEVELS = [ + "NONE", + "VERY_WEAK", + "WEAK", + "AVERAGE", + "GOOD", + "VERY_GOOD", + "EXCELLENT", +] as const; + +type SkillLevel = (typeof SKILL_LEVELS)[number]; + +type UpsertComputerSkillDTO = Partial<{ + pcUsage: SkillLevel; + word: SkillLevel; + excel: SkillLevel; + powerPoint: SkillLevel; + rahkaran: SkillLevel; + kasra: SkillLevel; + didgah: SkillLevel; + his: SkillLevel; + otherSoftware: string | null; +}>; + +class ComputerSkillServiceClass { + async getAll() { + // فقط برای ادمین/اپراتور (اگر نیاز داری) + return await ComputerSkill.findAll({order: [["createdAt", "DESC"]]}); + } + + async getMine(applicantId: string) { + const record = await ComputerSkill.findOne({where: {applicantId}}); + // می‌تونی null برگردونی یا خطا بدی؛ من null برمی‌گردونم تا فرانت بدونه هنوز پر نشده + return record; + } + + async upsertMine(applicantId: string, data: UpsertComputerSkillDTO) { + const existing = await ComputerSkill.findOne({where: {applicantId}}); + + if (!existing) { + return await ComputerSkill.create({ + applicantId, + ...data, + }); + } + + await existing.update(data); + return existing; + } + + async removeMine(applicantId: string) { + const existing = await ComputerSkill.findOne({where: {applicantId}}); + if (!existing) { + // اگر بخوای idempotent باشه، می‌تونی true برگردونی + throw new Error("رکورد مهارت‌های کامپیوتری یافت نشد"); + } + await existing.destroy(); + return true; + } +} +const ComputerSkillService = new ComputerSkillServiceClass(); +export default ComputerSkillService; diff --git a/src/modules/forms/course/controller/courseForm.controller.ts b/src/modules/forms/course/controller/courseForm.controller.ts index 2d202bd..640c3fe 100644 --- a/src/modules/forms/course/controller/courseForm.controller.ts +++ b/src/modules/forms/course/controller/courseForm.controller.ts @@ -1,6 +1,6 @@ -import { Controller } from "../../../../core/controller/main.controller"; -import { ServerResponse } from "../../../../core/types"; -import { NextFunction } from "express"; +import {Controller} from "../../../../core/controller/main.controller"; +import {ServerResponse} from "../../../../core/types"; +import {NextFunction} from "express"; import CourseInfoService from "../service/courseForm.service"; class CourseInfoControllerClass extends Controller { @@ -12,7 +12,7 @@ class CourseInfoControllerClass extends Controller { async create(req: any, res: ServerResponse, next: NextFunction) { try { const course = await this.#service.create(req.body); - res.status(201).json({ status: 200, data: course }); + res.status(201).json({status: 200, data: course}); } catch (err) { next(err); } @@ -21,7 +21,7 @@ class CourseInfoControllerClass extends Controller { async getById(req: any, res: ServerResponse, next: NextFunction) { try { const course = await this.#service.getById(req.params.id); - res.json({ status: 200, data: course }); + res.json({status: 200, data: course}); } catch (err) { next(err); } @@ -29,9 +29,9 @@ class CourseInfoControllerClass extends Controller { async list(req: any, res: ServerResponse, next: NextFunction) { try { - const { applicantId } = req.query as { applicantId?: string }; + const {applicantId} = req.query as {applicantId?: string}; const courses = await this.#service.list(applicantId); - res.json({ status: 200, data: courses }); + res.json({status: 200, data: courses}); } catch (err) { next(err); } @@ -40,7 +40,7 @@ class CourseInfoControllerClass extends Controller { async update(req: any, res: ServerResponse, next: NextFunction) { try { const updated = await this.#service.update(req.params.id, req.body); - res.json({ status: 200, data: updated }); + res.json({status: 200, data: updated}); } catch (err) { next(err); } @@ -49,11 +49,35 @@ class CourseInfoControllerClass extends Controller { async delete(req: any, res: ServerResponse, next: NextFunction) { try { const result = await this.#service.delete(req.params.id); - res.json({ status: 200, data: result }); + res.json({status: 200, data: result}); } catch (err) { next(err); } } + async bulkSave(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user.id; // دریافت از میدل‌ور احراز هویت + const {items} = req.body; + + if (!Array.isArray(items)) { + return res.status(400).json({ + status: 400, + data: {}, + message: "فیلد items الزامی بوده و باید به صورت آرایه باشد.", + }); + } + + const courses = await this.#service.bulkSync(applicantId, items); + + return res.status(200).json({ + status: 200, + message: "دوره‌های آموزشی با موفقیت ذخیره شدند", + data: courses, + }); + } catch (error) { + next(error); + } + } } const CourseInfoController = new CourseInfoControllerClass(); diff --git a/src/modules/forms/course/service/courseForm.service.ts b/src/modules/forms/course/service/courseForm.service.ts index 4cd87e4..6505c4e 100644 --- a/src/modules/forms/course/service/courseForm.service.ts +++ b/src/modules/forms/course/service/courseForm.service.ts @@ -1,17 +1,14 @@ import createHttpError from "http-errors"; -import { Controller } from "../../../../core/controller/main.controller"; -import { Course } from "../../../../models/Course"; +import {Controller} from "../../../../core/controller/main.controller"; +import {Course} from "../../../../models/Course"; +import { sequelize } from "../../../../models"; export interface CoursePayload { - applicantId: string; - degree: string; - field: string; - university: string; - startYear?: number | null; - endYear?: number | null; - gpa?: number | null; - description?: string | null; - certificateImageId?: string | null; + title: string; + institution: string; + year: number; + duration: string; + description?: string; } class CourseServiceClass extends Controller { @@ -26,21 +23,43 @@ class CourseServiceClass extends Controller { } async list(applicantId?: string) { - const where = applicantId ? { applicantId } : {}; - return await Course.findAll({ where }); + const where = applicantId ? {applicantId} : {}; + return await Course.findAll({where}); } async update(id: string, data: any) { - const [affectedRows] = await Course.update(data, { where: { id } }); + const [affectedRows] = await Course.update(data, {where: {id}}); if (affectedRows === 0) throw new createHttpError.NotFound("دوره برای ویرایش یافت نشد"); return await Course.findByPk(id); } async delete(id: string) { - const deleted = await Course.destroy({ where: { id } }); + const deleted = await Course.destroy({where: {id}}); if (!deleted) throw new createHttpError.NotFound("دوره برای حذف یافت نشد"); - return { message: "دوره با موفقیت حذف شد" }; + return {message: "دوره با موفقیت حذف شد"}; + } + async bulkSync(applicantId: string, items: CoursePayload[]) { + return await sequelize.transaction(async (t) => { + // ۱. حذف تمام سوابق قبلیِ این کاربر خاص + await Course.destroy({ + where: {applicantId}, + transaction: t, + }); + + // ۲. آماده‌سازی دیتا برای ثبت (بدون id تا خودکار تولید شود) + const dataToInsert = items.map((item) => ({ + applicantId: applicantId, // امنیت: تحمیل آیدی کاربر احراز هویت شده + title: item.title, + institution: item.institution, + year: item.year, + duration: item.duration, + description: item.description, + })); + + // ۳. ثبت دسته‌جمعی + return await Course.bulkCreate(dataToInsert, {transaction: t}); + }); } } diff --git a/src/modules/forms/education/controller/educationForm.controller.ts b/src/modules/forms/education/controller/educationForm.controller.ts index 7c6a141..0ff933b 100644 --- a/src/modules/forms/education/controller/educationForm.controller.ts +++ b/src/modules/forms/education/controller/educationForm.controller.ts @@ -64,6 +64,27 @@ class EducationInfoControllerClass extends Controller { next(err); } } + + async bulkSave(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user.id; + const { items } = req.body; + + if (!Array.isArray(items)) { + throw new Error("داده‌های ارسالی باید به صورت آرایه (items) باشند"); + } + + const data = await this.#service.bulkSync(applicantId, items); + + return res.status(200).json({ + status: 200, + data, + message: "اطلاعات تحصیلی با موفقیت بروزرسانی شد", + }); + } catch (error) { + next(error); + } + } } const EducationInfoController = new EducationInfoControllerClass(); diff --git a/src/modules/forms/education/service/educationForm.service.ts b/src/modules/forms/education/service/educationForm.service.ts index a3bf44d..572c877 100644 --- a/src/modules/forms/education/service/educationForm.service.ts +++ b/src/modules/forms/education/service/educationForm.service.ts @@ -1,22 +1,32 @@ import createHttpError from "http-errors"; -import { Controller } from "../../../../core/controller/main.controller"; -import { Education } from "../../../../models/Education"; - +import {Controller} from "../../../../core/controller/main.controller"; +import {Education, EducationAttributes} from "../../../../models/Education"; +import {sequelize} from "../../../../models"; export interface EducationPayload { applicantId: string; degree: string; field: string; university: string; - startYear?: number | null; - endYear?: number | null; - gpa?: number | null; - description?: string | null; - certificateImageId?: string | null; + startYear?: number; + endYear?: number; + gpa?: number; + description?: string; + certificateImageId?: string; } class EducationServiceClass extends Controller { - // ایجاد رکورد جدید + private validate(data: EducationPayload) { + if ( + typeof data.startYear === "number" && + typeof data.endYear === "number" && + data.endYear < data.startYear + ) { + throw new Error("سال پایان تحصیل نمی‌تواند کمتر از سال شروع باشد"); + } + return data; + } + // ایجاد رکورد جدید async create(data: EducationPayload) { try { const education = await Education.create({ @@ -29,7 +39,7 @@ class EducationServiceClass extends Controller { gpa: data.gpa ?? undefined, description: data.description ?? "", certificateImageId: data.certificateImageId ?? undefined, - }); + }); return education; } catch (err) { @@ -51,14 +61,14 @@ class EducationServiceClass extends Controller { } // لیست سوابق، مثلا بر اساس applicantId - async list(filter?: { applicantId?: string }) { + async list(filter?: {applicantId?: string}) { const where: any = {}; if (filter?.applicantId) { where.applicantId = filter.applicantId; } - const list = await Education.findAll({ where }); + const list = await Education.findAll({where}); return list; } @@ -106,13 +116,35 @@ class EducationServiceClass extends Controller { try { await education.destroy(); - return { success: true }; + return {success: true}; } catch (err) { - throw new createHttpError.InternalServerError( - "خطا در حذف سابقه تحصیلی", - ); + throw new createHttpError.InternalServerError("خطا در حذف سابقه تحصیلی"); } } + async bulkSync(applicantId: string, items: EducationPayload[]) { + return await sequelize.transaction(async (t) => { + // ۱. حذف رکوردهای قبلی + await Education.destroy({where: {applicantId}, transaction: t}); + + // ۲. آماده‌سازی دیتای جدید (اجبار به استفاده از applicantId صحیح) + const dataToInsert = items.map((item) => ({ + // استخراج تمام فیلدها به جز applicantId احتمالی در ورودی + degree: item.degree, + field: item.field, + university: item.university, + startYear: item.startYear, + endYear: item.endYear, + gpa: item.gpa, + description: item.description, + certificateImageId: item.certificateImageId, + // اعمال آیدی احراز هویت شده + applicantId: applicantId, + })); + + // ۳. ثبت گروهی + return await Education.bulkCreate(dataToInsert, {transaction: t}); + }); + } } const EducationService = new EducationServiceClass(); diff --git a/src/modules/forms/index.routes.ts b/src/modules/forms/index.routes.ts index 2ed42ef..84fd15c 100644 --- a/src/modules/forms/index.routes.ts +++ b/src/modules/forms/index.routes.ts @@ -1,9 +1,15 @@ -import { Router } from "express"; +import {Router} from "express"; import PersonalInfoRouter from "./personalInfo/index.routes"; import identityRouter from "./identity/index.routes"; import PhysicalInfoRouter from "./physicalInfo/index.routes"; import EducationInfoRouter from "./education/index.routes"; import CourseInfoRouter from "./course/index.routes"; +import CenterRouter from "./center/routes/center.routes"; +import referralRouter from "./referral/routes/referral.routes"; +import JobRequestRouter from "./jobRequest/routes/jobRequest.routes"; +import computerSkillRouter from "./computerSkill/routes/computerSkill.routes"; +import LanguageSkillRouter from "./languageSkill/routes/languageSkill.routes"; +import workExperienceRouter from "./workExperience/routes/workExperience.routes"; const formRouter = Router(); @@ -12,5 +18,11 @@ formRouter.use("/personal-info", PersonalInfoRouter); formRouter.use("/physical-info", PhysicalInfoRouter); formRouter.use("/education", EducationInfoRouter); formRouter.use("/course", CourseInfoRouter); +formRouter.use("/center", CenterRouter); +formRouter.use("/referral", referralRouter); +formRouter.use("/job-request", JobRequestRouter); +formRouter.use("/computer-skill", computerSkillRouter); +formRouter.use("/language-skill", LanguageSkillRouter); +formRouter.use("/work-experience", workExperienceRouter); export default formRouter; diff --git a/src/modules/forms/jobRequest/controller/jobRequest.controller.ts b/src/modules/forms/jobRequest/controller/jobRequest.controller.ts new file mode 100644 index 0000000..ac6e952 --- /dev/null +++ b/src/modules/forms/jobRequest/controller/jobRequest.controller.ts @@ -0,0 +1,76 @@ +// controllers/jobRequest.controller.ts +import { NextFunction } from "express"; +import { Controller } from "../../../../core/controller/main.controller"; +import { ServerResponse } from "../../../../core/types"; +import JobRequestService from "../service/jobRequest.service"; + +class JobRequestControllerClass extends Controller { + #service; + + constructor() { + super(); + this.#service = JobRequestService; + } + + // ثبت نهایی فرم انتخاب مراکز + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; // شناسه از توکن + // فرض: فرانت‌ند آرایه‌ای از انتخاب‌ها را می‌فرستد: [{centerId, categoryId, jobTitle, ...}, ...] + const requests = Array.isArray(req.body) ? req.body : [req.body]; + + const data = await this.#service.createRequests(applicantId, requests); + + return res.status(201).json({ + status: 201, + data, + message: "درخواست‌های شغلی با موفقیت ثبت شد", + }); + } catch (error) { + next(error); + } + } + + // مشاهده انتخاب‌های من + async getMyRequests(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getByApplicant(req.user?.id); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.update(req.params.id, req.user?.id, req.body); + return res.status(200).json({ + status: 200, + data, + message: "ویرایش شد", + }); + } catch (error) { + next(error); + } + } + + async remove(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.remove(req.params.id, req.user?.id); + return res.status(200).json({ + status: 200, + data:{}, + message: "حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const JobRequestController = new JobRequestControllerClass(); +export default JobRequestController; diff --git a/src/modules/forms/jobRequest/routes/jobRequest.routes.ts b/src/modules/forms/jobRequest/routes/jobRequest.routes.ts new file mode 100644 index 0000000..dac8845 --- /dev/null +++ b/src/modules/forms/jobRequest/routes/jobRequest.routes.ts @@ -0,0 +1,16 @@ +import { Router } from "express"; +import JobRequestController from "../controller/jobRequest.controller"; + +const JobRequestRouter = Router(); + +// ثبت انتخاب‌ها (مراکز و رسته‌ها) +JobRequestRouter.post("/", JobRequestController.create.bind(JobRequestController)); + +// لیست انتخاب‌های متقاضی فعلی +JobRequestRouter.get("/my-selections", JobRequestController.getMyRequests.bind(JobRequestController)); + +// حذف یا ویرایش یک انتخاب خاص +JobRequestRouter.put("/:id", JobRequestController.update.bind(JobRequestController)); +JobRequestRouter.delete("/:id", JobRequestController.remove.bind(JobRequestController)); + +export default JobRequestRouter; diff --git a/src/modules/forms/jobRequest/service/jobRequest.service.ts b/src/modules/forms/jobRequest/service/jobRequest.service.ts new file mode 100644 index 0000000..3b9537b --- /dev/null +++ b/src/modules/forms/jobRequest/service/jobRequest.service.ts @@ -0,0 +1,53 @@ +import { JobRequest } from "../../../../models/JobRequest"; + +class JobRequestServiceClass { + // ثبت درخواست‌های شغلی (پشتیبانی از ثبت همزمان چند مرکز/رسته) + async createRequests(applicantId: string, requests: any[]) { + // اضافه کردن applicantId به تمام آبجکت‌های ارسالی از سمت فرانت + const preparedData = requests.map((req) => ({ + ...req, + applicantId, + })); + + return await JobRequest.bulkCreate(preparedData); + } + + // دریافت لیست تمام درخواست‌های یک متقاضی (با جزئیات) + async getByApplicant(applicantId: string) { + return await JobRequest.findAll({ + where: {applicantId}, + // پیشنهاد: اینجا می‌توانید Include کنید تا نام مرکز و رسته را هم بگیرید + include: ["center", "category"], + order: [["createdAt", "DESC"]], + }); + } + + // دریافت تمام درخواست‌های ثبت شده برای یک مرکز خاص (برای پنل مدیریت مرکز) + async getByCenter(centerId: string) { + return await JobRequest.findAll({ + where: {centerId}, + include: ["applicant"], // اطلاعات متقاضی را هم بیاورد + }); + } + + // ویرایش یک درخواست خاص + async update(id: string, applicantId: string, data: any) { + const request = await JobRequest.findOne({where: {id, applicantId}}); + if (!request) throw new Error("درخواست یافت نشد."); + + return await request.update(data); + } + + // حذف درخواست + async remove(id: string, applicantId: string) { + const request = await JobRequest.findOne({where: {id, applicantId}}); + if (!request) throw new Error("درخواست یافت نشد."); + + await request.destroy(); + return {message: "درخواست با موفقیت حذف شد."}; + } +} + +const JobRequestService = new JobRequestServiceClass(); + +export default JobRequestService; diff --git a/src/modules/forms/languageSkill/controller/languageSkill.controller.ts b/src/modules/forms/languageSkill/controller/languageSkill.controller.ts new file mode 100644 index 0000000..ff24dbb --- /dev/null +++ b/src/modules/forms/languageSkill/controller/languageSkill.controller.ts @@ -0,0 +1,98 @@ +// modules/.../language-skill/controller/languageSkill.controller.ts +import { NextFunction } from "express"; +import { Controller } from "../../../../core/controller/main.controller"; +import { ServerResponse } from "../../../../core/types"; +import LanguageSkillService from "../service/langugeSkill.service"; + +class LanguageSkillControllerClass extends Controller { + #service; + + constructor() { + super(); + this.#service = LanguageSkillService; + } + + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const data = await this.#service.create(applicantId, req.body); + + return res.status(201).json({ + status: 201, + data, + message: "مهارت زبان با موفقیت ثبت شد", + }); + } catch (error) { + next(error); + } + } + + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const data = await this.#service.getAllByApplicant(applicantId); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + + async getOne(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const { id } = req.params; + + const data = await this.#service.getById(id, applicantId); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const { id } = req.params; + + const data = await this.#service.update(id, applicantId, req.body); + + return res.status(200).json({ + status: 200, + data, + message: "مهارت زبان ویرایش شد", + }); + } catch (error) { + next(error); + } + } + + async remove(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const { id } = req.params; + + await this.#service.remove(id, applicantId); + + return res.status(200).json({ + status: 200, + data: {}, + message: "مهارت زبان حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const LanguageSkillController = new LanguageSkillControllerClass(); +export default LanguageSkillController; diff --git a/src/modules/forms/languageSkill/routes/languageSkill.routes.ts b/src/modules/forms/languageSkill/routes/languageSkill.routes.ts new file mode 100644 index 0000000..6b19aa5 --- /dev/null +++ b/src/modules/forms/languageSkill/routes/languageSkill.routes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import LanguageSkillController from "../controller/languageSkill.controller"; + +const LanguageSkillRouter = Router(); + +LanguageSkillRouter.post("/", LanguageSkillController.create.bind(LanguageSkillController)); +LanguageSkillRouter.get("/", LanguageSkillController.getAll.bind(LanguageSkillController)); +LanguageSkillRouter.get("/:id", LanguageSkillController.getOne.bind(LanguageSkillController)); +LanguageSkillRouter.put("/:id", LanguageSkillController.update.bind(LanguageSkillController)); +LanguageSkillRouter.delete("/:id", LanguageSkillController.remove.bind(LanguageSkillController)); + +export default LanguageSkillRouter; diff --git a/src/modules/forms/languageSkill/service/langugeSkill.service.ts b/src/modules/forms/languageSkill/service/langugeSkill.service.ts new file mode 100644 index 0000000..ba68363 --- /dev/null +++ b/src/modules/forms/languageSkill/service/langugeSkill.service.ts @@ -0,0 +1,69 @@ +import { LanguageSkill } from "../../../../models/LanguageSkill"; + +type CreateLanguageSkillDTO = { + languageName?: string; + proficiency?: string; + hasCertificate?: boolean; + certificateType?: string; + description?: string; +}; + +type UpdateLanguageSkillDTO = Partial; + +class LanguageSkillServiceClass { + async create(applicantId: string, data: CreateLanguageSkillDTO) { + return await LanguageSkill.create({ + applicantId, + ...data, + }); + } + + async getAllByApplicant(applicantId: string) { + return await LanguageSkill.findAll({ + where: { applicantId }, + order: [["createdAt", "DESC"]], + }); + } + + async getById(id: string, applicantId: string) { + const record = await LanguageSkill.findOne({ + where: { id, applicantId }, + }); + + if (!record) { + throw new Error("رکورد مهارت زبان یافت نشد"); + } + + return record; + } + + async update(id: string, applicantId: string, data: UpdateLanguageSkillDTO) { + const record = await LanguageSkill.findOne({ + where: { id, applicantId }, + }); + + if (!record) { + throw new Error("رکورد مهارت زبان یافت نشد"); + } + + await record.update(data); + return record; + } + + async remove(id: string, applicantId: string) { + const record = await LanguageSkill.findOne({ + where: { id, applicantId }, + }); + + if (!record) { + throw new Error("رکورد مهارت زبان یافت نشد"); + } + + await record.destroy(); + return true; + } +} + +const LanguageSkillService = new LanguageSkillServiceClass() + +export default LanguageSkillService; diff --git a/src/modules/forms/referral/controller/referral.controller.ts b/src/modules/forms/referral/controller/referral.controller.ts new file mode 100644 index 0000000..a853c22 --- /dev/null +++ b/src/modules/forms/referral/controller/referral.controller.ts @@ -0,0 +1,94 @@ +// controllers/referral.controller.ts +import { NextFunction } from "express"; +import { Controller } from "../../../../core/controller/main.controller"; +import { ServerResponse } from "../../../../core/types"; +import ReferralService from "../service/referral.service"; + +class ReferralControllerClass extends Controller { + #service; + + constructor() { + super(); + // فرض بر این است که ReferralService به صورت singleton (New شده) اکسپورت شده است. + // در غیر این صورت: this.#service = new ReferralService(); + this.#service = ReferralService; + } + + // دریافت لیست معرف‌های کاربر فعلی + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + // استخراج آی‌دی متقاضی از توکن برای امنیت بیشتر (فقط معرف‌های خودش را ببیند) + const applicantId = req.user?.id || req.query?.applicantId; + + const data = await this.#service.getAll(applicantId); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); // اضافه شدن هندلر خطا که قبلاً خالی بود + } + } + + // ثبت معرف جدید + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; // شناسه متقاضی از توکن امنیتی + + const data = await this.#service.create({ + ...req.body, + applicantId: applicantId || req.body.applicantId // اولویت با توکن است + }); + + return res.status(201).json({ + status: 201, + data, + message: "ساخته شد", + }); + } catch (error) { + next(error); + } + } + + // ویرایش اطلاعات معرف با بررسی امنیت مالکیت رکورد + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req?.params?.id; + const applicantId = req.user?.id; // برای بررسی اینکه کاربر فقط معرف خودش را ادیت کند + + const data = await this.#service.update(id, applicantId, req.body); + + return res.status(200).json({ // تغییر وضعیت به 200 (OK) + status: 200, + data, + message: "ویرایش شد", + }); + } catch (error) { + next(error); + } + } + + // حذف معرف با بررسی مالکیت رکورد + async remove(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req?.params?.id; + const applicantId = req.user?.id; // برای جلوگیری از حذف معرف‌های دیگران توسط کاربران غریبه + + await this.#service.delete(id, applicantId); + + return res.status(200).json({ // تغییر وضعیت به 200 (OK) + status: 200, + data: {}, + message: "حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const ReferralController = new ReferralControllerClass(); + +export default ReferralController; diff --git a/src/modules/forms/referral/routes/referral.routes.ts b/src/modules/forms/referral/routes/referral.routes.ts new file mode 100644 index 0000000..aa15f66 --- /dev/null +++ b/src/modules/forms/referral/routes/referral.routes.ts @@ -0,0 +1,16 @@ +// routes/referral.routes.ts +import { Router } from "express"; +import ReferralController from "../controller/referral.controller"; + + +const ReferralRouter = Router(); + +// تمامی عملیات‌ها نیاز به لاگین دارند +// ReferralRouter.use(authMiddleware); + +ReferralRouter.post("/", ReferralController.create); +ReferralRouter.get("/", ReferralController.getAll); +ReferralRouter.put("/:id", ReferralController.update); +ReferralRouter.delete("/:id", ReferralController.remove); + +export default ReferralRouter; diff --git a/src/modules/forms/referral/service/referral.service.ts b/src/modules/forms/referral/service/referral.service.ts new file mode 100644 index 0000000..cb24f4a --- /dev/null +++ b/src/modules/forms/referral/service/referral.service.ts @@ -0,0 +1,41 @@ +import {Referral} from "../../../../models/Referral"; + +class ReferralServiceClass { + // ثبت معرف جدید + async create(data: any) { + return await Referral.create(data); + } + async getAll(applicantId?: string) { + const filter = applicantId ? {where: {applicantId}} : {}; + return await Referral.findAll(filter); + } + // دریافت تمام معرف‌های یک متقاضی خاص + async getByApplicant(applicantId: string) { + return await Referral.findAll({ + where: {applicantId}, + order: [["createdAt", "DESC"]], + }); + } + + // بروزرسانی اطلاعات معرف + async update(id: string, applicantId: string, data: any) { + const referral = await Referral.findOne({where: {id, applicantId}}); + if (!referral) + throw new Error("معرف مورد نظر یافت نشد یا دسترسی مجاز نیست."); + + return await referral.update(data); + } + + // حذف معرف + async delete(id: string, applicantId: string) { + const referral = await Referral.findOne({where: {id, applicantId}}); + if (!referral) throw new Error("معرف مورد نظر یافت نشد."); + + await referral.destroy(); + return {message: "معرف با موفقیت حذف شد."}; + } +} + +const ReferralService = new ReferralServiceClass(); + +export default ReferralService; diff --git a/src/modules/forms/relation/controller/relation.controller.ts b/src/modules/forms/relation/controller/relation.controller.ts new file mode 100644 index 0000000..924d39c --- /dev/null +++ b/src/modules/forms/relation/controller/relation.controller.ts @@ -0,0 +1,81 @@ +// modules/relation/controller/relation.controller.ts +import { NextFunction } from "express"; + +import RelationService from "../service/relation.service"; +import { ServerResponse } from "../../../../core/types"; +import { Controller } from "../../../../core/controller/main.controller"; + +class RelationControllerClass extends Controller { + #service; + + constructor() { + super(); + this.#service = RelationService; + } + + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user.id; + const data = await this.#service.create(applicantId, req.body); + + return res.status(201).json({ + status: 201, + data, + message: "اطلاعات آشنایان با موفقیت ثبت شد", + }); + } catch (error) { + next(error); + } + } + + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user.id; + const data = await this.#service.getAllByApplicant(applicantId); + + return res.status(200).json({ status: 200, data, message: "Ok" }); + } catch (error) { + next(error); + } + } + + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const { id } = req.params; + const applicantId = req.user.id; + const data = await this.#service.update(id, applicantId, req.body); + + return res.status(200).json({ status: 200, data, message: "بروزرسانی انجام شد" }); + } catch (error) { + next(error); + } + } + + async remove(req: any, res: ServerResponse, next: NextFunction) { + try { + const { id } = req.params; + const applicantId = req.user.id; + await this.#service.remove(id, applicantId); + + return res.status(200).json({ status: 200,data:{}, message: "رکورد حذف شد" }); + } catch (error) { + next(error); + } + } + + // متد ثبت گروهی (Batch) + async bulkSave(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user.id; + const { relations } = req.body; // انتظار یک آرایه داریم + const data = await this.#service.bulkSync(applicantId, relations); + + return res.status(200).json({ status: 200, data, message: "اطلاعات همگام‌سازی شد" }); + } catch (error) { + next(error); + } + } +} + +const RelationController = new RelationControllerClass(); +export default RelationController; diff --git a/src/modules/forms/relation/routes/relation.routes.ts b/src/modules/forms/relation/routes/relation.routes.ts new file mode 100644 index 0000000..a56db9e --- /dev/null +++ b/src/modules/forms/relation/routes/relation.routes.ts @@ -0,0 +1,16 @@ +// modules/relation/routes/relation.routes.ts +import { Router } from "express"; +import RelationController from "../controller/relation.controller"; + +const relationRouter = Router(); + +// مسیرهای اصلی +relationRouter.post("/", RelationController.create.bind(RelationController)); +relationRouter.get("/", RelationController.getAll.bind(RelationController)); +relationRouter.put("/:id", RelationController.update.bind(RelationController)); +relationRouter.delete("/:id", RelationController.remove.bind(RelationController)); + +// مسیر کمکی برای ثبت همزمان چند آشنا +relationRouter.post("/bulk", RelationController.bulkSave.bind(RelationController)); + +export default relationRouter; diff --git a/src/modules/forms/relation/service/relation.service.ts b/src/modules/forms/relation/service/relation.service.ts new file mode 100644 index 0000000..8f86e2b --- /dev/null +++ b/src/modules/forms/relation/service/relation.service.ts @@ -0,0 +1,79 @@ +import { Relation } from "../../../../models/Relation"; + + +type CreateRelationDTO = { + firstName: string; + lastName: string; + relationship: string; + jobTitle?: string; + workplaceName?: string; + phoneNumber: string; + order: number; +}; + +class RelationServiceClass { + async create(applicantId: string, data: CreateRelationDTO) { + // بررسی اینکه آیا این اولویت (order) قبلاً برای این متقاضی ثبت شده یا خیر + const existingOrder = await Relation.findOne({ + where: {applicantId, order: data.order}, + }); + + if (existingOrder) { + throw new Error( + `اطلاعات برای اولویت ${data.order} قبلاً ثبت شده است. از متد آپدیت استفاده کنید.`, + ); + } + + return await Relation.create({ + applicantId, + ...data, + }); + } + + async getAllByApplicant(applicantId: string) { + return await Relation.findAll({ + where: {applicantId}, + order: [["order", "ASC"]], // مرتب‌سازی بر اساس اولویت ۱، ۲ و... + }); + } + + async update( + id: string, + applicantId: string, + data: Partial, + ) { + const relation = await Relation.findOne({ + where: {id, applicantId}, + }); + + if (!relation) throw new Error("رکورد مورد نظر یافت نشد."); + + await relation.update(data); + return relation; + } + + async remove(id: string, applicantId: string) { + const relation = await Relation.findOne({ + where: {id, applicantId}, + }); + + if (!relation) throw new Error("رکورد مورد نظر یافت نشد."); + + await relation.destroy(); + return true; + } + + /** + * ثبت گروهی آشنایان (مثلاً هر دو آشنا در یک مرحله ارسال شوند) + */ + async bulkSync(applicantId: string, relations: CreateRelationDTO[]) { + // ابتدا رکوردهای قبلی را پاک می‌کنیم (یا می‌توان منطق بروزرسانی پیچیده تری داشت) + await Relation.destroy({where: {applicantId}}); + + const dataWithId = relations.map((r) => ({...r, applicantId})); + return await Relation.bulkCreate(dataWithId); + } +} + +const RelationService = new RelationServiceClass() +export default RelationService; diff --git a/src/modules/forms/workExperience/controller/workExperience.controller.ts b/src/modules/forms/workExperience/controller/workExperience.controller.ts new file mode 100644 index 0000000..2c5851d --- /dev/null +++ b/src/modules/forms/workExperience/controller/workExperience.controller.ts @@ -0,0 +1,130 @@ +// modules/.../work-experience/controller/workExperience.controller.ts +import {NextFunction} from "express"; +import {Controller} from "../../../../core/controller/main.controller"; +import {ServerResponse} from "../../../../core/types"; +import WorkExperienceService from "../service/workExperience.service"; + +class WorkExperienceControllerClass extends Controller { + #service; + + constructor() { + super(); + this.#service = WorkExperienceService; + } + + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const data = await this.#service.create(applicantId, req.body); + + return res.status(201).json({ + status: 201, + data, + message: "سابقه کاری ثبت شد", + }); + } catch (error) { + next(error); + } + } + + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const data = await this.#service.getAllByApplicant(applicantId); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + + async getOne(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const {id} = req.params; + + const data = await this.#service.getById(id, applicantId); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const {id} = req.params; + + const data = await this.#service.update(id, applicantId, req.body); + + return res.status(200).json({ + status: 200, + data, + message: "سابقه کاری ویرایش شد", + }); + } catch (error) { + next(error); + } + } + + async remove(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const {id} = req.params; + + await this.#service.remove(id, applicantId); + + return res.status(200).json({ + status: 200, + data: {}, + message: "سابقه کاری حذف شد", + }); + } catch (error) { + next(error); + } + } + + // اختیاری: endpoint جدا برای "هیچ سابقه‌ای ندارم" + async setHasNoExperience(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user?.id; + const data = await this.#service.setHasNoExperience(applicantId); + + return res.status(200).json({ + status: 200, + data, + message: "وضعیت «بدون سابقه کاری» ثبت شد", + }); + } catch (error) { + next(error); + } + } + async bulkSave(req: any, res: ServerResponse, next: NextFunction) { + try { + const applicantId = req.user.id; + const {items} = req.body; + + const data = await this.#service.bulkSync(applicantId, items); + + return res.status(200).json({ + status: 200, + data, + message: "سوابق کاری با موفقیت ذخیره شدند", + }); + } catch (error) { + next(error); + } + } +} + +const WorkExperienceController = new WorkExperienceControllerClass(); +export default WorkExperienceController; diff --git a/src/modules/forms/workExperience/routes/workExperience.routes.ts b/src/modules/forms/workExperience/routes/workExperience.routes.ts new file mode 100644 index 0000000..4a732d0 --- /dev/null +++ b/src/modules/forms/workExperience/routes/workExperience.routes.ts @@ -0,0 +1,17 @@ +// modules/.../work-experience/routes/workExperience.routes.ts +import { Router } from "express"; +import WorkExperienceController from "../controller/workExperience.controller"; + +const workExperienceRouter = Router(); + +workExperienceRouter.post("/", WorkExperienceController.create.bind(WorkExperienceController)); +workExperienceRouter.get("/", WorkExperienceController.getAll.bind(WorkExperienceController)); +workExperienceRouter.get("/:id", WorkExperienceController.getOne.bind(WorkExperienceController)); +workExperienceRouter.put("/:id", WorkExperienceController.update.bind(WorkExperienceController)); +workExperienceRouter.delete("/:id", WorkExperienceController.remove.bind(WorkExperienceController)); + +// اختیاری: +workExperienceRouter.post("/no-experience", WorkExperienceController.setHasNoExperience.bind(WorkExperienceController)); +workExperienceRouter.post("/bulk", WorkExperienceController.bulkSave.bind(WorkExperienceController)); + +export default workExperienceRouter; diff --git a/src/modules/forms/workExperience/service/workExperience.service.ts b/src/modules/forms/workExperience/service/workExperience.service.ts new file mode 100644 index 0000000..1ba615f --- /dev/null +++ b/src/modules/forms/workExperience/service/workExperience.service.ts @@ -0,0 +1,119 @@ +import {WorkExperience} from "../../../../models/WorkExperience"; + +type CreateWorkExperienceDTO = { + hasNoExperience?: boolean; + companyName?: string; + lastPosition?: string; + startYear?: number; + endYear?: number; + leavingReason?: string; + description?: string; +}; + +type UpdateWorkExperienceDTO = Partial; + +class WorkExperienceService { + private normalize(data: UpdateWorkExperienceDTO) { + const normalized: UpdateWorkExperienceDTO = {...data}; + + // اگر کاربر گفت هیچ سابقه‌ای ندارم، اطلاعات سابقه را خالی کن + if (normalized.hasNoExperience === true) { + normalized.companyName = undefined; + normalized.lastPosition = undefined; + normalized.startYear = undefined; + normalized.endYear = undefined; + normalized.leavingReason = undefined; + normalized.description = undefined; + } + + // اگر endYear < startYear شد، خطا بده (کنترل پایه) + if ( + typeof normalized.startYear === "number" && + typeof normalized.endYear === "number" && + normalized.endYear < normalized.startYear + ) { + throw new Error("سال پایان نمی‌تواند کمتر از سال شروع باشد"); + } + + return normalized; + } + + async create(applicantId: string, data: CreateWorkExperienceDTO) { + const normalized = this.normalize(data); + + return await WorkExperience.create({ + applicantId, + ...normalized, + }); + } + + async getAllByApplicant(applicantId: string) { + return await WorkExperience.findAll({ + where: {applicantId}, + order: [["createdAt", "DESC"]], + }); + } + + async getById(id: string, applicantId: string) { + const record = await WorkExperience.findOne({ + where: {id, applicantId}, + }); + + if (!record) throw new Error("سابقه کاری یافت نشد"); + return record; + } + + async update(id: string, applicantId: string, data: UpdateWorkExperienceDTO) { + const record = await WorkExperience.findOne({ + where: {id, applicantId}, + }); + + if (!record) throw new Error("سابقه کاری یافت نشد"); + + const normalized = this.normalize(data); + await record.update(normalized); + return record; + } + + async remove(id: string, applicantId: string) { + const record = await WorkExperience.findOne({ + where: {id, applicantId}, + }); + + if (!record) throw new Error("سابقه کاری یافت نشد"); + + await record.destroy(); + return true; + } + + /** + * کمکی برای سناریویی که کاربر می‌زند "هیچ سابقه‌ای ندارم": + * می‌تونی همه سوابق قبلی‌اش را پاک کنی و یک رکورد فلگ‌دار بسازی. + * (اختیاری، اگر در UX شما لازم است) + */ + async setHasNoExperience(applicantId: string) { + // پاک کردن سوابق قبلی + await WorkExperience.destroy({where: {applicantId}}); + + // ساخت یک رکورد فلگ‌دار + return await WorkExperience.create({ + applicantId, + hasNoExperience: true, + }); + } + + async bulkSync(applicantId: string, items: CreateWorkExperienceDTO[]) { + await WorkExperience.destroy({where: {applicantId}}); + + if (!items || items.length === 0) return []; + + const normalizedItems = items.map((item) => ({ + applicantId, + ...this.normalize(item), + })); + + return await WorkExperience.bulkCreate(normalizedItems); + } +} + +export default new WorkExperienceService(); diff --git a/src/modules/job-category/controller/jobCategory.controller.ts b/src/modules/job-category/controller/jobCategory.controller.ts new file mode 100644 index 0000000..cf21a17 --- /dev/null +++ b/src/modules/job-category/controller/jobCategory.controller.ts @@ -0,0 +1,90 @@ +import {NextFunction} from "express"; + +import JobCategoryService from "../service/jobCategory.service"; +import {ServerResponse} from "../../../core/types"; +import {Controller} from "../../../core/controller/main.controller"; + +class JobCategoryControllerClass extends Controller { + #service; + + constructor() { + super(); + this.#service = JobCategoryService; + } + + 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) { + 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 { + const data = await this.#service.create(req.body); + + return res.status(201).json({ + status: 201, + data, + message: "ساخته شد", + }); + } catch (error) { + next(error); + } + } + + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req?.params?.id; + const data = await this.#service.update(id, req.body); + + return res.status(200).json({ + status: 200, + 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 JobCategoryController = new JobCategoryControllerClass(); + +export default JobCategoryController; diff --git a/src/modules/job-category/routes/jobCategory.routes.ts b/src/modules/job-category/routes/jobCategory.routes.ts new file mode 100644 index 0000000..365b1ed --- /dev/null +++ b/src/modules/job-category/routes/jobCategory.routes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import JobCategoryController from "../controller/jobCategory.controller"; + +const JobCategoryRouter = Router(); + +JobCategoryRouter.get("/", JobCategoryController.getAll.bind(JobCategoryController)); +JobCategoryRouter.get("/:id", JobCategoryController.getById.bind(JobCategoryController)); +JobCategoryRouter.post("/", JobCategoryController.create.bind(JobCategoryController)); +JobCategoryRouter.put("/:id", JobCategoryController.update.bind(JobCategoryController)); +JobCategoryRouter.delete("/:id", JobCategoryController.remove.bind(JobCategoryController)); + +export default JobCategoryRouter; diff --git a/src/modules/job-category/service/jobCategory.service.ts b/src/modules/job-category/service/jobCategory.service.ts new file mode 100644 index 0000000..4a43c55 --- /dev/null +++ b/src/modules/job-category/service/jobCategory.service.ts @@ -0,0 +1,45 @@ +import { JobCategory } from "../../../models/JobCategory"; + +class JobCategoryServiceClass { + async getAll() { + return await JobCategory.findAll({ + order: [["createdAt", "DESC"]], + }); + } + + async getById(id: string) { + const item = await JobCategory.findByPk(id); + if (!item) { + throw new Error("رسته شغلی یافت نشد"); + } + return item; + } + + async create(data: any) { + return await JobCategory.create(data); + } + + async update(id: string, data: any) { + const item = await JobCategory.findByPk(id); + if (!item) { + throw new Error("رسته شغلی یافت نشد"); + } + + await item.update(data); + return item; + } + + async remove(id: string) { + const item = await JobCategory.findByPk(id); + if (!item) { + throw new Error("رسته شغلی یافت نشد"); + } + + await item.destroy(); + return true; + } +} + +const JobCategoryService = new JobCategoryServiceClass(); + +export default JobCategoryService; diff --git a/src/modules/user/controller/user.controller.ts b/src/modules/user/controller/user.controller.ts new file mode 100644 index 0000000..10c2bfa --- /dev/null +++ b/src/modules/user/controller/user.controller.ts @@ -0,0 +1,93 @@ +// src/modules/user/controller/user.controller.ts +import { NextFunction } from "express"; +import { Controller } from "../../../core/controller/main.controller"; +import { ServerResponse } from "../../../core/types"; +import UserService from "../service/user.service"; + +class UserControllerClass extends Controller { + #service; + + constructor() { + super(); + this.#service = UserService; + } + + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.create(req.body); + return res.status(201).json({ status: 201, data, message: "کاربر ساخته شد" }); + } catch (error) { + next(error); + } + } + + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAll({ + search: req.query.search as string | undefined, + isActive: req.query.isActive !== undefined ? req.query.isActive === "true" : undefined, + }); + + return res.status(200).json({ status: 200, data, message: "Ok" }); + } catch (error) { + next(error); + } + } + + async getOne(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getById(req.params.id); + return res.status(200).json({ status: 200, data, message: "Ok" }); + } catch (error) { + next(error); + } + } + + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.update(req.params.id, req.body); + return res.status(200).json({ status: 200, data, message: "آپدیت شد" }); + } catch (error) { + next(error); + } + } + + async toggleActive(req: any, res: ServerResponse, next: NextFunction) { + try { + const { isActive } = req.body as { isActive: boolean }; + const data = await this.#service.toggleActive(req.params.id, isActive); + return res.status(200).json({ status: 200, data, message: "وضعیت بروزرسانی شد" }); + } catch (error) { + next(error); + } + } + + async remove(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.remove(req.params.id); + return res.status(200).json({ status: 200, data: {}, message: "حذف شد" }); + } catch (error) { + next(error); + } + } + + // Auth: لاگین + async login(req: any, res: ServerResponse, next: NextFunction) { + try { + const { email, password } = req.body; + const user = await this.#service.login(email, password); + + // اینجا JWT بسازید و برگردانید (اگر دارید) + return res.status(200).json({ + status: 200, + data: { user }, + message: "ورود موفق", + }); + } catch (error) { + next(error); + } + } +} + +const UserController = new UserControllerClass(); +export default UserController; diff --git a/src/modules/user/routes/user.routes.ts b/src/modules/user/routes/user.routes.ts new file mode 100644 index 0000000..1d31714 --- /dev/null +++ b/src/modules/user/routes/user.routes.ts @@ -0,0 +1,18 @@ +// src/modules/user/routes/user.routes.ts +import { Router } from "express"; +import UserController from "../controller/user.controller"; + +const userRouter = Router(); + +// Auth +userRouter.post("/login", UserController.login.bind(UserController)); + +// CRUD (معمولاً باید پشت adminMiddleware باشد) +userRouter.post("/", UserController.create.bind(UserController)); +userRouter.get("/", UserController.getAll.bind(UserController)); +userRouter.get("/:id", UserController.getOne.bind(UserController)); +userRouter.put("/:id", UserController.update.bind(UserController)); +userRouter.patch("/:id/active", UserController.toggleActive.bind(UserController)); +userRouter.delete("/:id", UserController.remove.bind(UserController)); + +export default userRouter; diff --git a/src/modules/user/service/user.service.ts b/src/modules/user/service/user.service.ts new file mode 100644 index 0000000..b8bda6c --- /dev/null +++ b/src/modules/user/service/user.service.ts @@ -0,0 +1,132 @@ +// src/modules/user/service/user.service.ts +import bcrypt from "bcryptjs"; +import { Op } from "sequelize"; +import { User } from "../../../models/User"; + +type CreateUserDTO = { + fullname: string; + email: string; + password: string; + roleId: string; + isActive?: boolean; +}; + +type UpdateUserDTO = Partial<{ + fullname: string; + email: string; + password: string; + roleId: string; + isActive: boolean; +}>; + +class UserServiceClass { + private async hashPassword(password: string) { + const saltRounds = 10; + return await bcrypt.hash(password, saltRounds); + } + + private sanitize(user: User) { + const json = user.toJSON() as any; + delete json.password; + return json; + } + + async create(data: CreateUserDTO) { + const exists = await User.findOne({ where: { email: data.email } }); + if (exists) throw new Error("ایمیل تکراری است"); + + const hashed = await this.hashPassword(data.password); + + const user = await User.create({ + fullname: data.fullname, + email: data.email, + password: hashed, + roleId: data.roleId, + isActive: data.isActive ?? true, + }); + + return this.sanitize(user); + } + + async getAll(query?: { search?: string; isActive?: boolean }) { + const where: any = {}; + + if (typeof query?.isActive === "boolean") where.isActive = query.isActive; + + if (query?.search) { + where[Op.or] = [ + { fullname: { [Op.iLike]: `%${query.search}%` } }, + { email: { [Op.iLike]: `%${query.search}%` } }, + ]; + } + + const users = await User.findAll({ + where, + order: [["createdAt", "DESC"]], + // اگر relation Role را attach کرده‌اید: + // include: [{ association: "role" }], + }); + + return users.map((u:any) => this.sanitize(u)); + } + + async getById(id: string) { + const user = await User.findByPk(id); + if (!user) throw new Error("کاربر یافت نشد"); + return this.sanitize(user); + } + + async update(id: string, data: UpdateUserDTO) { + const user = await User.findByPk(id); + if (!user) throw new Error("کاربر یافت نشد"); + + // اگر ایمیل تغییر می‌کند، تکراری نبودن را چک کن + if (data.email && data.email !== user.email) { + const exists = await User.findOne({ where: { email: data.email } }); + if (exists) throw new Error("ایمیل تکراری است"); + } + + const updatePayload: any = { ...data }; + + if (typeof data.password === "string" && data.password.trim().length > 0) { + updatePayload.password = await this.hashPassword(data.password); + } else { + delete updatePayload.password; + } + + await user.update(updatePayload); + return this.sanitize(user); + } + + async toggleActive(id: string, isActive: boolean) { + const user = await User.findByPk(id); + if (!user) throw new Error("کاربر یافت نشد"); + + await user.update({ isActive }); + return this.sanitize(user); + } + + async remove(id: string) { + const user = await User.findByPk(id); + if (!user) throw new Error("کاربر یافت نشد"); + await user.destroy(); + return true; + } + + // Auth: لاگین با ایمیل و پسورد + async login(email: string, password: string) { + const user = await User.findOne({ where: { email } }); + if (!user) throw new Error("ایمیل یا رمز عبور اشتباه است"); + + if (!user.isActive) throw new Error("حساب کاربری غیرفعال است"); + + const ok = await bcrypt.compare(password, user.password); + if (!ok) throw new Error("ایمیل یا رمز عبور اشتباه است"); + + // اینجا معمولاً JWT صادر می‌کنید؛ من فقط user sanitize برمی‌گردونم + return this.sanitize(user); + } +} + +const UserService = new UserServiceClass() +export default UserService;