diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..9e49cb4 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,20 @@ +{ + "configurations": [ + { + "name": "Win32", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "compilerPath": "C:\\msys64\\ucrt64\\bin\\gcc.exe", + "cStandard": "c23", + "cppStandard": "gnu++17", + "intelliSenseMode": "windows-gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 71f28d1..11ff914 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,7 +18,6 @@ "cloudinary": "^1.27.1", "cors": "^2.8.5", "debug": "^4.2.0", - "dotenv": "^8.2.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "express": "^4.17.2", @@ -56,6 +55,7 @@ "babel-eslint": "^10.1.0", "babel-plugin-module-resolver": "^4.1.0", "babel-plugin-transform-runtime": "^6.23.0", + "dotenv": "^17.4.2", "eslint": "^7.3.1", "eslint-config-airbnb-base": "^14.2.0", "eslint-plugin-import": "^2.21.2", @@ -120,6 +120,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.16.7", "@babel/generator": "^7.16.8", @@ -2611,7 +2612,8 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.1", @@ -3718,6 +3720,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.19.0" } @@ -3799,6 +3802,7 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4602,19 +4606,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/c12/node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/c12/node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -5431,8 +5422,7 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dargs": { "version": "7.0.0", @@ -5625,7 +5615,8 @@ "version": "0.0.1312386", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dicer": { "version": "0.2.5", @@ -5733,11 +5724,16 @@ } }, "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "devOptional": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/duck": { @@ -6189,6 +6185,7 @@ "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -6316,6 +6313,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.4", "array.prototype.flat": "^1.2.5", @@ -7602,6 +7600,7 @@ "integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -8771,7 +8770,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "peer": true }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -9022,7 +9022,6 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -10440,6 +10439,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -10465,6 +10465,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", @@ -11166,7 +11167,6 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -11310,8 +11310,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/selderee": { "version": "0.11.0", @@ -12201,6 +12200,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", diff --git a/backend/package.json b/backend/package.json index 49a2ca7..5975146 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,7 +32,6 @@ "cloudinary": "^1.27.1", "cors": "^2.8.5", "debug": "^4.2.0", - "dotenv": "^8.2.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "express": "^4.17.2", @@ -70,6 +69,7 @@ "babel-eslint": "^10.1.0", "babel-plugin-module-resolver": "^4.1.0", "babel-plugin-transform-runtime": "^6.23.0", + "dotenv": "^17.4.2", "eslint": "^7.3.1", "eslint-config-airbnb-base": "^14.2.0", "eslint-plugin-import": "^2.21.2", diff --git a/backend/src/core/api/index.js b/backend/src/core/api/index.js index 5e0f725..5906f8e 100644 --- a/backend/src/core/api/index.js +++ b/backend/src/core/api/index.js @@ -3,6 +3,7 @@ import { UserResolver } from 'core/api/user/user.resolver'; import { ApiDocument } from 'core/config/swagger.config'; import { LawyerResolver } from 'core/api/lawyer/lawyer.resolver'; import { UploadResolver } from 'core/api/upload'; +import { ReportsResolver } from './reports/reports.resolver'; import { TemplateResolver } from 'core/api/template'; import { DocumentResolver } from 'core/api/document'; import { HandlerResolver } from '../../packages/handler/HandlerResolver'; @@ -22,6 +23,7 @@ export const ModuleResolver = HandlerResolver MediaResolver, LawyerResolver, UploadResolver, + ReportsResolver, LawResolver, TemplateResolver, DocumentResolver, diff --git a/backend/src/core/api/lawyer/lawyer.resolver.js b/backend/src/core/api/lawyer/lawyer.resolver.js index dd619e9..89ff569 100644 --- a/backend/src/core/api/lawyer/lawyer.resolver.js +++ b/backend/src/core/api/lawyer/lawyer.resolver.js @@ -1,7 +1,7 @@ import { Module } from 'packages/handler/Module'; import { RecordIdInterceptor } from 'core/modules/interceptor/recordId/record-id.interceptor'; import { LawyerController } from './lawyer.controller'; -import { DefaultQueryCriteriaDocument } from '../../common/swagger/filter'; +import { DefaultQueryCriteriaDocument } from '../../common/swagger/lawyerFilter'; import { RecordId } from '../../common/swagger/record-id'; import { UpdateLawyerProfileInterceptor } from 'core/modules/lawyer/interceptor'; import { hasLawyerRole } from 'core/modules/auth/guard'; diff --git a/backend/src/core/api/reports/reports.controller.js b/backend/src/core/api/reports/reports.controller.js new file mode 100644 index 0000000..39bd703 --- /dev/null +++ b/backend/src/core/api/reports/reports.controller.js @@ -0,0 +1,28 @@ +import { ReportsService } from '../../modules/reports/service/reports.service'; +import { ListReportsDto } from '../../modules/reports'; +import { ValidHttpResponse } from '../../../packages/handler/response/validHttp.response'; + +class Controller { + constructor() { + this.service = ReportsService; + } + + listReports = async req => { + const data = await this.service.listReports(req.query); + return ValidHttpResponse.toOkResponse(data); + } + getReportsStats = async req => { + const data = await this.service.getReportsStats(req.query); + return ValidHttpResponse.toOkResponse(data); + } + getReportById = async req => { + const data = await this.service.getReportById(req.params.id); + return ValidHttpResponse.toOkResponse(data); + } + getReportsHistory = async req => { + const data = await this.service.getReportsHistory(req.query); + return ValidHttpResponse.toOkResponse(data); + } +} + +export const ReportsController = new Controller(); diff --git a/backend/src/core/api/reports/reports.resolver.js b/backend/src/core/api/reports/reports.resolver.js new file mode 100644 index 0000000..32f4192 --- /dev/null +++ b/backend/src/core/api/reports/reports.resolver.js @@ -0,0 +1,54 @@ +import { + GetReportsInterceptor +} from 'core/modules/reports'; +import { Module } from 'packages/handler/Module'; +import { ReportsController } from './reports.controller'; +import { authMiddleware } from 'core/modules/auth/middleware/auth.middleware'; +import { uploadMediaSwagger } from 'core/common/swagger'; +import { MediaInterceptor } from 'core/modules/document'; +import { hasAdminRole } from 'core/modules/auth/guard'; +import { ListReportsParams, GetReportsStatsParams, GetReportsHistoryParams } from 'core/common/swagger/reportFilter'; +import { RecordIdInterceptor } from 'core/modules/interceptor/recordId/record-id.interceptor'; +import { RecordId } from 'core/common/swagger/record-id'; + +export const ReportsResolver = Module.builder() + .addPrefix({ + prefixPath: '/reports', + tag: 'reports', + module: 'ReportsModule' + }) + .register([ + { + route: '', + method: 'get', + params: ListReportsParams, + guards: [hasAdminRole], + controller: ReportsController.listReports, + preAuthorization: true, + }, + { + route: '/:id', + method: 'get', + guards: [hasAdminRole], + params: [RecordId], + interceptor: [RecordIdInterceptor], + controller: ReportsController.getReportById, + preAuthorization: true, + }, + { + route: '/stats', + method: 'get', + params: GetReportsStatsParams, + guards: [hasAdminRole], + controller: ReportsController.getReportsStats, + preAuthorization: true, + }, + { + route: '/history', + method: 'get', + params: GetReportsHistoryParams, + guards: [hasAdminRole], + controller: ReportsController.getReportsHistory, + preAuthorization: true, + }, + ]); diff --git a/backend/src/core/api/user/user.resolver.js b/backend/src/core/api/user/user.resolver.js index db54629..ba435c2 100644 --- a/backend/src/core/api/user/user.resolver.js +++ b/backend/src/core/api/user/user.resolver.js @@ -9,7 +9,7 @@ import { hasAdminRole } from 'core/modules/auth/guard'; import { RecordIdInterceptor } from 'core/modules/interceptor/recordId/record-id.interceptor'; import { UserController } from './user.controller'; import { RecordId } from '../../common/swagger/record-id'; -import { DefaultQueryCriteriaDocument } from '../../common/swagger/filter'; +import { DefaultQueryCriteriaDocument } from '../../common/swagger/lawyerFilter'; export const UserResolver = Module.builder() .addPrefix({ diff --git a/backend/src/core/common/swagger/index.js b/backend/src/core/common/swagger/index.js index abbd86b..a53538c 100644 --- a/backend/src/core/common/swagger/index.js +++ b/backend/src/core/common/swagger/index.js @@ -1,3 +1,3 @@ -export * from './filter'; +export * from './lawyerFilter'; export * from './record-id'; export * from './upload-file'; diff --git a/backend/src/core/common/swagger/filter.js b/backend/src/core/common/swagger/lawyerFilter.js similarity index 100% rename from backend/src/core/common/swagger/filter.js rename to backend/src/core/common/swagger/lawyerFilter.js diff --git a/backend/src/core/common/swagger/reportFilter.js b/backend/src/core/common/swagger/reportFilter.js new file mode 100644 index 0000000..643f2f8 --- /dev/null +++ b/backend/src/core/common/swagger/reportFilter.js @@ -0,0 +1,71 @@ +import { SwaggerDocument } from '../../../packages/swagger'; + +export const ListReportsCriteriaDocument = { + page: (desc = 'Max page: 100000. Default: 1') => SwaggerDocument.ApiParams({ + name: 'page', + paramsIn: 'query', + required: true, + type: 'int', + description: desc + }), + limit: (desc = 'Max size: 200. Default: 20') => SwaggerDocument.ApiParams({ + name: 'limit', + paramsIn: 'query', + required: true, + type: 'int', + description: desc + }), + status: (desc = 'Of two types: pending and resolved') => SwaggerDocument.ApiParams({ + name: 'status', + paramsIn: 'query', + required: true, + type: 'string', + description: desc + }), + startDate: desc => SwaggerDocument.ApiParams({ + name: 'startDate', + paramsIn: 'query', + required: true, + type: 'date', + description: desc + }), + endDate: desc => SwaggerDocument.ApiParams({ + name: 'endDate', + paramsIn: 'query', + required: true, + type: 'date', + description: desc + }) +}; + +export const GetReportsStatsCriteriaDocument = { + month: (desc = 'Get reports statistics of month. Format: YYYY-MM') => SwaggerDocument.ApiParams({ + name: 'month', + paramsIn: 'query', + required: true, + type: 'date', + description: desc + }) +}; + +export const GetReportsHistoryCriteriaDocument = { + page: (desc = 'Page number (starts from 1)') => SwaggerDocument.ApiParams({ + name: 'page', + paramsIn: 'query', + required: true, + type: 'int', + description: desc + }), + limit: (desc = 'Number of reports per page (max 100)') => SwaggerDocument.ApiParams({ + name: 'limit', + paramsIn: 'query', + required: true, + type: 'int', + description: desc + }), +}; + + +export const ListReportsParams = [...Object.values(ListReportsCriteriaDocument).map(exec => exec())]; +export const GetReportsStatsParams = [...Object.values(GetReportsStatsCriteriaDocument).map(exec => exec())]; +export const GetReportsHistoryParams = [...Object.values(GetReportsHistoryCriteriaDocument).map(exec => exec())]; \ No newline at end of file diff --git a/backend/src/core/modules/reports/dto/getReportsHistory.dto.js b/backend/src/core/modules/reports/dto/getReportsHistory.dto.js new file mode 100644 index 0000000..b742932 --- /dev/null +++ b/backend/src/core/modules/reports/dto/getReportsHistory.dto.js @@ -0,0 +1,12 @@ +import { ApiDocument } from 'core/config/swagger.config'; +import { SwaggerDocument } from 'packages/swagger'; + +ApiDocument.addModel('GetReportsHistoryDto', { + page: SwaggerDocument.ApiProperty({ type: 'string' }), + limit: SwaggerDocument.ApiProperty({ type: 'string' }), +}); + +export const GetReportsHistoryDto = body => ({ + page: body.page, + limit: body.limit +}); \ No newline at end of file diff --git a/backend/src/core/modules/reports/dto/getReportsStats.dto.js b/backend/src/core/modules/reports/dto/getReportsStats.dto.js new file mode 100644 index 0000000..2378e56 --- /dev/null +++ b/backend/src/core/modules/reports/dto/getReportsStats.dto.js @@ -0,0 +1,10 @@ +import { ApiDocument } from 'core/config/swagger.config'; +import { SwaggerDocument } from 'packages/swagger'; + +ApiDocument.addModel('GetReportsStatsDto', { + month: SwaggerDocument.ApiProperty({ type: 'string' }) +}); + +export const GetReportsStatsDto = body => ({ + month: body.month +}); \ No newline at end of file diff --git a/backend/src/core/modules/reports/dto/index.js b/backend/src/core/modules/reports/dto/index.js new file mode 100644 index 0000000..52a017e --- /dev/null +++ b/backend/src/core/modules/reports/dto/index.js @@ -0,0 +1,3 @@ +export * from './listReports.dto'; +export * from './getReportsStats.dto'; +export * from './getReportsHistory.dto'; \ No newline at end of file diff --git a/backend/src/core/modules/reports/dto/listReports.dto.js b/backend/src/core/modules/reports/dto/listReports.dto.js new file mode 100644 index 0000000..ff2d193 --- /dev/null +++ b/backend/src/core/modules/reports/dto/listReports.dto.js @@ -0,0 +1,18 @@ +import { ApiDocument } from 'core/config/swagger.config'; +import { SwaggerDocument } from 'packages/swagger'; + +ApiDocument.addModel('ListReportsDto', { + page: SwaggerDocument.ApiProperty({ type: 'string' }), + limit: SwaggerDocument.ApiProperty({ type: 'string' }), + status: SwaggerDocument.ApiProperty({ type: 'string' }), + startDate: SwaggerDocument.ApiProperty({ type: 'string' }), + endDate: SwaggerDocument.ApiProperty({ type: 'string' }), +}); + +export const ListReportsDto = body => ({ + page: body.page, + limit: body.limit, + status: body.status, + startDate: body.startDate, + endDate: body.endDate, +}); \ No newline at end of file diff --git a/backend/src/core/modules/reports/index.js b/backend/src/core/modules/reports/index.js new file mode 100644 index 0000000..cc5f8c8 --- /dev/null +++ b/backend/src/core/modules/reports/index.js @@ -0,0 +1,3 @@ +export * from './dto'; +export * from './interceptor'; +export * from './service'; \ No newline at end of file diff --git a/backend/src/core/modules/reports/interceptor/getReportsHistory.interceptor.js b/backend/src/core/modules/reports/interceptor/getReportsHistory.interceptor.js new file mode 100644 index 0000000..7cb5a6c --- /dev/null +++ b/backend/src/core/modules/reports/interceptor/getReportsHistory.interceptor.js @@ -0,0 +1,10 @@ +import { DefaultValidatorInterceptor } from 'core/infrastructure/interceptor'; +import { JoiUtils } from 'core/utils'; +import Joi from 'joi'; + +export const GetReportsHistoryInterceptor = new DefaultValidatorInterceptor( + Joi.object({ + page: JoiUtils.positiveNumber().min(1).default(1).required(), + limit: JoiUtils.positiveNumber().min(1).max(100).default(20).required(), + }) +); \ No newline at end of file diff --git a/backend/src/core/modules/reports/interceptor/getReportsStats.interceptor.js b/backend/src/core/modules/reports/interceptor/getReportsStats.interceptor.js new file mode 100644 index 0000000..129ab96 --- /dev/null +++ b/backend/src/core/modules/reports/interceptor/getReportsStats.interceptor.js @@ -0,0 +1,11 @@ +import { DefaultValidatorInterceptor } from 'core/infrastructure/interceptor'; +import { JoiUtils } from 'core/utils'; +import Joi from 'joi'; + +const DATE_YYYY_MM_FORMAT = /^\d{4}-\d{2}$/; + +export const GetReportsStatsInterceptor = new DefaultValidatorInterceptor( + Joi.object({ + month: Joi.string().regex(DATE_YYYY_MM_FORMAT), + }) +); \ No newline at end of file diff --git a/backend/src/core/modules/reports/interceptor/index.js b/backend/src/core/modules/reports/interceptor/index.js new file mode 100644 index 0000000..c7d19ce --- /dev/null +++ b/backend/src/core/modules/reports/interceptor/index.js @@ -0,0 +1,3 @@ +export * from './listReports.interceptor'; +export * from './getReportsStats.interceptor'; +export * from './getReportsHistory.interceptor'; \ No newline at end of file diff --git a/backend/src/core/modules/reports/interceptor/listReports.interceptor.js b/backend/src/core/modules/reports/interceptor/listReports.interceptor.js new file mode 100644 index 0000000..dda2dca --- /dev/null +++ b/backend/src/core/modules/reports/interceptor/listReports.interceptor.js @@ -0,0 +1,13 @@ +import { DefaultValidatorInterceptor } from 'core/infrastructure/interceptor'; +import { JoiUtils } from 'core/utils'; +import Joi from 'joi'; + +export const ListReportsInterceptor = new DefaultValidatorInterceptor( + Joi.object({ + page: JoiUtils.positiveNumber().min(1).default(1).required(), + limit: JoiUtils.positiveNumber().min(1).max(100).default(20).required(), + status: JoiUtils.requiredString(), + startDate: JoiUtils.date(), + endDate: JoiUtils.date(), + }) +); \ No newline at end of file diff --git a/backend/src/core/modules/reports/reports.repository.js b/backend/src/core/modules/reports/reports.repository.js new file mode 100644 index 0000000..26e1ba5 --- /dev/null +++ b/backend/src/core/modules/reports/reports.repository.js @@ -0,0 +1,65 @@ +import connection from 'core/database'; + +class Repository { + async getReportsByDate(startDate, endDate) { + return connection.reports.findMany({ + where: { + created_at: { + gte: startDate, + lte: endDate + }, + deleted_at: null, + }, + select: { + status: true, + target_user_id: true, + reporter_id: true, + reason: true, + } + }); + } + async getReportsByDateAndStatus(status, startDate, endDate) { + return connection.reports.findMany({ + where: { + status: status, + created_at: { + gte: startDate, + lte: endDate + }, + deleted_at: null, + }, + select: { + target_user_id: true, + reporter_id: true, + reason: true, + } + }); + } + + async getReportById(id) { + return connection.reports.findFirst({ + where: { target_user_id: id, deleted_at: null } + }); + } + + async getReportsHistory(page, limit) { + return connection.reports.findMany({ + where: { + deleted_at: null, + }, + orderBy: { + created_at: 'asc', + }, + select: { + target_user_id: true, + reporter_id: true, + reason: true, + status: true, + }, + skip: (page - 1) * limit, + take: limit, + }); + } +} + +export const ReportsRepository = new Repository(); \ No newline at end of file diff --git a/backend/src/core/modules/reports/service/index.js b/backend/src/core/modules/reports/service/index.js new file mode 100644 index 0000000..9de9ea5 --- /dev/null +++ b/backend/src/core/modules/reports/service/index.js @@ -0,0 +1 @@ +export * from './reports.service'; \ No newline at end of file diff --git a/backend/src/core/modules/reports/service/reports.service.js b/backend/src/core/modules/reports/service/reports.service.js new file mode 100644 index 0000000..349f3ba --- /dev/null +++ b/backend/src/core/modules/reports/service/reports.service.js @@ -0,0 +1,110 @@ +import crypto from 'crypto'; +import { MailerService } from 'core/common/mailer.service'; +import { JwtPayload } from 'core/modules/auth/dto/jwt-sign.dto'; +import { ReportsRepository } from '../reports.repository'; +// import { BcryptService } from './bcrypt.service'; +// import { JwtService } from './jwt.service'; +import { UnAuthorizedException, DuplicateException, BadRequestException } from '../../../../packages/httpException'; +import { DefaultQueryCriteriaDocument } from 'core/common/swagger/reportFilter'; + +class Service { + constructor() { + this.repository = ReportsRepository; + } + + async listReports(listReportsDto) { + let allStatus = ["pending", "resolved"]; + if (allStatus.indexOf(listReportsDto.status) == -1) { + throw new BadRequestException("Invalid Status value"); + } + + let startDate = listReportsDto.startDate + ? new Date(listReportsDto.startDate).toISOString() + : null; + let endDate = listReportsDto.endDate + ? new Date(listReportsDto.endDate).toISOString() + : null; + + if (startDate > endDate) { + throw new BadRequestException("startDate must be before endDate"); + } + + let reports = await this.repository.getReportsByDateAndStatus(listReportsDto.status, startDate, endDate); + reports = reports.slice((listReportsDto.page - 1) * listReportsDto.limit, listReportsDto.page * listReportsDto.limit); + return { + data: { + items: { + reports: reports, + }, + pagination: { + page: listReportsDto.page, + limit: listReportsDto.limit, + total: reports.length, + totalPages: Math.ceil(reports.length / listReportsDto.limit), + } + } + }; + } + + async getReportsStats(getReportsStatsDto) { + if (getReportsStatsDto.month.length == 0) getReportsStatsDto.month = new Date().now().toISOString().slice(0, 8); + let year = parseInt(getReportsStatsDto.month.slice(0, 5)); + let month = parseInt(getReportsStatsDto.month.slice(6)); + let startDate = new Date(year + '-' + month + "-01").toISOString(); + year += (month + 1) % 12; + month = (month + 1) % 12; + + let endDate = new Date(new Date(year + '-' + month + "-01") - 1); + + let reports = await this.repository.getReportsByDate(startDate, endDate); + let resolvedReports = 0; + for (let i = 0; i < reports.length; i ++) { + if (reports[i].status == "RESOLVED") resolvedReports ++; + } + + return { + data: { + month: getReportsStatsDto.month, + newReports: reports.length, + resolved: resolvedReports + } + }; + } + + async getReportById(id) { + let report = await this.repository.getReportById(id); + if (!report) { + throw new BadRequestException("Report not found"); + } + + return { + data: { + targetUserId: report.target_user_id, + reporterId: report.reporter_id, + reason: report.reason, + status: report.status, + createdAt: report.created_at, + updatedAt: report.updated_at, + } + } + } + + async getReportsHistory(getReportsHistoryDto) { + let reports = await this.repository.getReportsHistory(parseInt(getReportsHistoryDto.page), parseInt(getReportsHistoryDto.limit)); + return { + data: { + items: { + reports: reports, + }, + pagination: { + page: parseInt(getReportsHistoryDto.page), + limit: parseInt(getReportsHistoryDto.limit), + total: reports.length, + totalPages: Math.ceil(reports.length / parseInt(getReportsHistoryDto.limit)), + } + } + }; + } +} + +export const ReportsService = new Service(); diff --git a/backend/yarn.lock b/backend/yarn.lock index f1bcdfc..70bb640 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2848,16 +2848,11 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" -dotenv@^17.3.1: +dotenv@^17.3.1, dotenv@^17.4.2: version "17.4.2" resolved "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz" integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== -dotenv@^8.2.0: - version "8.6.0" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - duck@^0.1.12: version "0.1.12" resolved "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz"