fix
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm run lint)"
|
||||
"Bash(npm run lint)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm run migration:generate:*)",
|
||||
"Bash(npm run migration:run:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(npx typeorm migration:generate:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
53
package-lock.json
generated
53
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/multer": "^1.4.12",
|
||||
"axios": "^1.11.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
@@ -5030,9 +5031,19 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||
@@ -6917,7 +6928,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
@@ -7364,7 +7374,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@@ -7650,7 +7659,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -8388,6 +8396,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@@ -8433,15 +8461,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"dev": true,
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8462,7 +8490,6 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -8472,7 +8499,6 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
@@ -8917,7 +8943,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -11861,6 +11886,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/multer": "^1.4.12",
|
||||
"axios": "^1.11.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
|
||||
@@ -14,7 +14,7 @@ import { VideoAdModule } from 'src/video-ad/video-ad.module';
|
||||
UserModule,
|
||||
LineAdModule,
|
||||
PosterAdModule,
|
||||
VideoAdModule
|
||||
VideoAdModule,
|
||||
],
|
||||
controllers: [AdCommentController],
|
||||
providers: [AdCommentService],
|
||||
|
||||
@@ -152,11 +152,11 @@ export class AdCommentService {
|
||||
isActive: boolean = true,
|
||||
): Promise<AdComment[]> {
|
||||
console.log('adId', adId);
|
||||
let where: FindOptionsWhere<AdComment> = {};
|
||||
const where: FindOptionsWhere<AdComment> = {};
|
||||
if (isActive) {
|
||||
where.isActive = true;
|
||||
}
|
||||
let relations = ['user'];
|
||||
const relations = ['user'];
|
||||
if (adType === AdType.LINE) {
|
||||
where.lineAd = { id: adId };
|
||||
relations.push('lineAd');
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { AdPositionService } from './ad-position.service';
|
||||
import { CreateAdPositionDto } from './dto/create-ad-position.dto';
|
||||
import { UpdateAdPositionDto } from './dto/update-ad-position.dto';
|
||||
@@ -7,13 +18,13 @@ import { PositionType } from 'src/common/enums/position-type.enum';
|
||||
import {
|
||||
AdSlotsOverviewResponseDto,
|
||||
LineAdsResponseDto,
|
||||
SlotDetailsResponseDto
|
||||
SlotDetailsResponseDto,
|
||||
} from './dto/ad-slots-overview-response.dto';
|
||||
import {
|
||||
DateBasedAdSlotsOverviewDto,
|
||||
DateBasedLineAdsDto,
|
||||
AvailableDatesDto,
|
||||
DateBasedSlotDetailsDto
|
||||
DateBasedSlotDetailsDto,
|
||||
} from './dto/date-based-ad-slots.dto';
|
||||
|
||||
@Controller('ad-position')
|
||||
@@ -36,7 +47,10 @@ export class AdPositionController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateAdPositionDto: UpdateAdPositionDto) {
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateAdPositionDto: UpdateAdPositionDto,
|
||||
) {
|
||||
return this.adPositionService.update(id, updateAdPositionDto);
|
||||
}
|
||||
|
||||
@@ -52,25 +66,28 @@ export class AdPositionController {
|
||||
@Get('ad-slots/overview')
|
||||
async getAdSlotsOverview(
|
||||
@Query('pageType') pageType?: PageType,
|
||||
@Query('category') category?: string
|
||||
@Query('category') category?: string,
|
||||
): Promise<AdSlotsOverviewResponseDto> {
|
||||
try {
|
||||
// Validate pageType if provided
|
||||
if (pageType && !Object.values(PageType).includes(pageType)) {
|
||||
throw new HttpException(
|
||||
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.adPositionService.getAdSlotsOverview(pageType, category);
|
||||
|
||||
return await this.adPositionService.getAdSlotsOverview(
|
||||
pageType,
|
||||
category,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to fetch ad slots overview',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -82,14 +99,14 @@ export class AdPositionController {
|
||||
@Get('ad-slots/line-ads')
|
||||
async getLineAds(
|
||||
@Query('pageType') pageType?: PageType,
|
||||
@Query('category') category?: string
|
||||
@Query('category') category?: string,
|
||||
): Promise<LineAdsResponseDto> {
|
||||
try {
|
||||
// Validate pageType if provided
|
||||
if (pageType && !Object.values(PageType).includes(pageType)) {
|
||||
throw new HttpException(
|
||||
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +117,7 @@ export class AdPositionController {
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to fetch line ads',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,14 +131,14 @@ export class AdPositionController {
|
||||
@Param('pageType') pageType: PageType,
|
||||
@Param('side') side: PositionType,
|
||||
@Param('position') position: string,
|
||||
@Query('category') category?: string
|
||||
@Query('category') category?: string,
|
||||
): Promise<SlotDetailsResponseDto> {
|
||||
try {
|
||||
// Validate pageType
|
||||
if (!Object.values(PageType).includes(pageType)) {
|
||||
throw new HttpException(
|
||||
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,7 +146,7 @@ export class AdPositionController {
|
||||
if (!Object.values(PositionType).includes(side)) {
|
||||
throw new HttpException(
|
||||
`Invalid side. Must be one of: ${Object.values(PositionType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,26 +155,35 @@ export class AdPositionController {
|
||||
if (isNaN(positionNum) || positionNum < 1 || positionNum > 6) {
|
||||
throw new HttpException(
|
||||
'Invalid position. Must be a number between 1 and 6',
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate position constraints for CENTER positions
|
||||
if ((side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) && positionNum !== 1) {
|
||||
if (
|
||||
(side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM) &&
|
||||
positionNum !== 1
|
||||
) {
|
||||
throw new HttpException(
|
||||
'CENTER_TOP and CENTER_BOTTOM positions only support position 1',
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.adPositionService.getSlotDetails(pageType, side, positionNum, category);
|
||||
return await this.adPositionService.getSlotDetails(
|
||||
pageType,
|
||||
side,
|
||||
positionNum,
|
||||
category,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to fetch slot details',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -176,7 +202,7 @@ export class AdPositionController {
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to fetch available dates',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -189,14 +215,14 @@ export class AdPositionController {
|
||||
async getAdSlotsByDate(
|
||||
@Query('date') date: string,
|
||||
@Query('pageType') pageType?: PageType,
|
||||
@Query('category') category?: string
|
||||
@Query('category') category?: string,
|
||||
): Promise<DateBasedAdSlotsOverviewDto> {
|
||||
try {
|
||||
// Validate date format
|
||||
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
throw new HttpException(
|
||||
'Invalid date format. Use YYYY-MM-DD',
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,18 +230,22 @@ export class AdPositionController {
|
||||
if (pageType && !Object.values(PageType).includes(pageType)) {
|
||||
throw new HttpException(
|
||||
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.adPositionService.getAdSlotsByDate(date, pageType, category);
|
||||
|
||||
return await this.adPositionService.getAdSlotsByDate(
|
||||
date,
|
||||
pageType,
|
||||
category,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to fetch ad slots by date',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -228,14 +258,14 @@ export class AdPositionController {
|
||||
async getLineAdsByDate(
|
||||
@Query('date') date: string,
|
||||
@Query('pageType') pageType?: PageType,
|
||||
@Query('category') category?: string
|
||||
@Query('category') category?: string,
|
||||
): Promise<DateBasedLineAdsDto> {
|
||||
try {
|
||||
// Validate date format
|
||||
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
throw new HttpException(
|
||||
'Invalid date format. Use YYYY-MM-DD',
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,18 +273,22 @@ export class AdPositionController {
|
||||
if (pageType && !Object.values(PageType).includes(pageType)) {
|
||||
throw new HttpException(
|
||||
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.adPositionService.getLineAdsByDate(date, pageType, category);
|
||||
return await this.adPositionService.getLineAdsByDate(
|
||||
date,
|
||||
pageType,
|
||||
category,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to fetch line ads by date',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -269,14 +303,14 @@ export class AdPositionController {
|
||||
@Param('pageType') pageType: PageType,
|
||||
@Param('side') side: PositionType,
|
||||
@Param('position') position: string,
|
||||
@Query('category') category?: string
|
||||
@Query('category') category?: string,
|
||||
): Promise<DateBasedSlotDetailsDto> {
|
||||
try {
|
||||
// Validate date format
|
||||
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
throw new HttpException(
|
||||
'Invalid date format. Use YYYY-MM-DD',
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -284,7 +318,7 @@ export class AdPositionController {
|
||||
if (!Object.values(PageType).includes(pageType)) {
|
||||
throw new HttpException(
|
||||
`Invalid pageType. Must be one of: ${Object.values(PageType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -292,7 +326,7 @@ export class AdPositionController {
|
||||
if (!Object.values(PositionType).includes(side)) {
|
||||
throw new HttpException(
|
||||
`Invalid side. Must be one of: ${Object.values(PositionType).join(', ')}`,
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -301,26 +335,36 @@ export class AdPositionController {
|
||||
if (isNaN(positionNum) || positionNum < 1 || positionNum > 6) {
|
||||
throw new HttpException(
|
||||
'Invalid position. Must be a number between 1 and 6',
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate position constraints for CENTER positions
|
||||
if ((side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) && positionNum !== 1) {
|
||||
if (
|
||||
(side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM) &&
|
||||
positionNum !== 1
|
||||
) {
|
||||
throw new HttpException(
|
||||
'CENTER_TOP and CENTER_BOTTOM positions only support position 1',
|
||||
HttpStatus.BAD_REQUEST
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.adPositionService.getSlotDetailsByDate(date, pageType, side, positionNum, category);
|
||||
return await this.adPositionService.getSlotDetailsByDate(
|
||||
date,
|
||||
pageType,
|
||||
side,
|
||||
positionNum,
|
||||
category,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new HttpException(
|
||||
'Failed to fetch slot details by date',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,15 @@ import { LineAd } from 'src/line-ad/entities/line-ad.entity';
|
||||
import { MainCategory } from 'src/category/entities/main-category.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AdPosition, PosterAd, VideoAd, LineAd, MainCategory])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
AdPosition,
|
||||
PosterAd,
|
||||
VideoAd,
|
||||
LineAd,
|
||||
MainCategory,
|
||||
]),
|
||||
],
|
||||
controllers: [AdPositionController],
|
||||
providers: [AdPositionService],
|
||||
exports: [AdPositionService],
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
LineAdsResponseDto,
|
||||
LineAdSummaryDto,
|
||||
SlotDetailsResponseDto,
|
||||
SlotAdDetailDto
|
||||
SlotAdDetailDto,
|
||||
} from './dto/ad-slots-overview-response.dto';
|
||||
import {
|
||||
DateBasedAdSlotsOverviewDto,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
AvailableDatesDto,
|
||||
DateBasedSlotDetailsDto,
|
||||
DateBasedSlotAdDetailDto,
|
||||
CategoryDto
|
||||
CategoryDto,
|
||||
} from './dto/date-based-ad-slots.dto';
|
||||
import { PosterAd } from 'src/poster-ad/entities/poster-ad.entity';
|
||||
import { VideoAd } from 'src/video-ad/entities/video-ad.entity';
|
||||
@@ -64,7 +64,10 @@ export class AdPositionService {
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, updateAdPositionDto: UpdateAdPositionDto): Promise<AdPosition|null> {
|
||||
async update(
|
||||
id: string,
|
||||
updateAdPositionDto: UpdateAdPositionDto,
|
||||
): Promise<AdPosition | null> {
|
||||
await this.adPositionRepository.update(id, updateAdPositionDto);
|
||||
return this.findOne(id);
|
||||
}
|
||||
@@ -80,7 +83,7 @@ export class AdPositionService {
|
||||
if (!ad.isActive || ad.status !== AdStatus.PUBLISHED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -101,14 +104,16 @@ export class AdPositionService {
|
||||
expectedStatus: AdStatus.PUBLISHED,
|
||||
statusMatch: ad?.status === AdStatus.PUBLISHED,
|
||||
datesLength: ad?.dates?.length,
|
||||
dates: ad?.dates
|
||||
dates: ad?.dates,
|
||||
});
|
||||
|
||||
|
||||
if (!ad.isActive || ad.status !== AdStatus.PUBLISHED) {
|
||||
console.log(`[DEBUG] Ad ${ad?.id} rejected - not active or not published`);
|
||||
console.log(
|
||||
`[DEBUG] Ad ${ad?.id} rejected - not active or not published`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
|
||||
console.log(`[DEBUG] Ad ${ad?.id} rejected - no valid dates array`);
|
||||
return false;
|
||||
@@ -117,16 +122,20 @@ export class AdPositionService {
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
const todayISOString = today.toISOString().split('T')[0];
|
||||
|
||||
console.log(`[DEBUG] Checking dates for ad ${ad?.id}. Today: ${todayISOString}`);
|
||||
|
||||
console.log(
|
||||
`[DEBUG] Checking dates for ad ${ad?.id}. Today: ${todayISOString}`,
|
||||
);
|
||||
|
||||
const isActiveToday = ad.dates.some((dateStr: string) => {
|
||||
const dateOnly = dateStr.split('T')[0];
|
||||
const matches = dateOnly === todayISOString;
|
||||
console.log(`[DEBUG] Date ${dateOnly} matches today ${todayISOString}: ${matches}`);
|
||||
console.log(
|
||||
`[DEBUG] Date ${dateOnly} matches today ${todayISOString}: ${matches}`,
|
||||
);
|
||||
return matches;
|
||||
});
|
||||
|
||||
|
||||
console.log(`[DEBUG] Ad ${ad?.id} is active today: ${isActiveToday}`);
|
||||
return isActiveToday;
|
||||
}
|
||||
@@ -138,11 +147,11 @@ export class AdPositionService {
|
||||
if (!Array.isArray(dates) || dates.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const latestDate = dates.reduce((latest, current) => {
|
||||
return new Date(current) > new Date(latest) ? current : latest;
|
||||
});
|
||||
|
||||
|
||||
return new Date(latestDate);
|
||||
}
|
||||
|
||||
@@ -153,7 +162,7 @@ export class AdPositionService {
|
||||
return {
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
color: category.categories_color || undefined
|
||||
color: category.categories_color || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,39 +171,60 @@ export class AdPositionService {
|
||||
*/
|
||||
private extractUniqueCategories(ads: any[]): CategoryDto[] {
|
||||
const categoriesMap = new Map<string, CategoryDto>();
|
||||
|
||||
ads.forEach(ad => {
|
||||
|
||||
ads.forEach((ad) => {
|
||||
if (ad.mainCategory) {
|
||||
categoriesMap.set(ad.mainCategory.id, this.categoryToDto(ad.mainCategory));
|
||||
categoriesMap.set(
|
||||
ad.mainCategory.id,
|
||||
this.categoryToDto(ad.mainCategory),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return Array.from(categoriesMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ad slots overview with occupancy status
|
||||
*/
|
||||
async getAdSlotsOverview(pageType?: PageType, category?: string): Promise<AdSlotsOverviewResponseDto> {
|
||||
async getAdSlotsOverview(
|
||||
pageType?: PageType,
|
||||
category?: string,
|
||||
): Promise<AdSlotsOverviewResponseDto> {
|
||||
const slots: SlotOccupancyDto[] = [];
|
||||
const pageTypes = pageType ? [pageType] : [PageType.HOME, PageType.CATEGORY];
|
||||
const sides = [PositionType.LEFT_SIDE, PositionType.RIGHT_SIDE, PositionType.CENTER_TOP, PositionType.CENTER_BOTTOM];
|
||||
const pageTypes = pageType
|
||||
? [pageType]
|
||||
: [PageType.HOME, PageType.CATEGORY];
|
||||
const sides = [
|
||||
PositionType.LEFT_SIDE,
|
||||
PositionType.RIGHT_SIDE,
|
||||
PositionType.CENTER_TOP,
|
||||
PositionType.CENTER_BOTTOM,
|
||||
];
|
||||
const maxCapacity = 5; // Maximum 5 ads per slot
|
||||
|
||||
for (const page of pageTypes) {
|
||||
for (const side of sides) {
|
||||
const maxPositions = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 1 : 6;
|
||||
|
||||
const maxPositions =
|
||||
side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM
|
||||
? 1
|
||||
: 6;
|
||||
|
||||
for (let position = 1; position <= maxPositions; position++) {
|
||||
// Get all ads for this slot
|
||||
const adPositions = await this.adPositionRepository.find({
|
||||
where: {
|
||||
pageType: page,
|
||||
side,
|
||||
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
|
||||
adType: AdType.POSTER // Only poster and video ads use positioned slots
|
||||
position:
|
||||
side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM
|
||||
? 0
|
||||
: position,
|
||||
adType: AdType.POSTER, // Only poster and video ads use positioned slots
|
||||
},
|
||||
relations: ['posterAd', 'videoAd']
|
||||
relations: ['posterAd', 'videoAd'],
|
||||
});
|
||||
|
||||
// Also get video ads for this slot
|
||||
@@ -202,16 +232,20 @@ export class AdPositionService {
|
||||
where: {
|
||||
pageType: page,
|
||||
side,
|
||||
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
|
||||
adType: AdType.VIDEO
|
||||
position:
|
||||
side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM
|
||||
? 0
|
||||
: position,
|
||||
adType: AdType.VIDEO,
|
||||
},
|
||||
relations: ['videoAd']
|
||||
relations: ['videoAd'],
|
||||
});
|
||||
|
||||
const allPositions = [...adPositions, ...videoAdPositions];
|
||||
|
||||
|
||||
// Filter for active ads
|
||||
const activeAds = allPositions.filter(pos => {
|
||||
const activeAds = allPositions.filter((pos) => {
|
||||
const ad = pos.posterAd || pos.videoAd;
|
||||
return ad && this.isAdActive(ad);
|
||||
});
|
||||
@@ -219,18 +253,22 @@ export class AdPositionService {
|
||||
// Calculate expiry dates
|
||||
let earliestExpiryDate: Date | undefined;
|
||||
let latestExpiryDate: Date | undefined;
|
||||
|
||||
|
||||
if (activeAds.length > 0) {
|
||||
const expiryDates = activeAds
|
||||
.map(pos => {
|
||||
.map((pos) => {
|
||||
const ad = pos.posterAd || pos.videoAd;
|
||||
return ad ? this.getExpiryDate(ad.dates) : undefined;
|
||||
})
|
||||
.filter(date => date !== undefined) as Date[];
|
||||
.filter((date) => date !== undefined);
|
||||
|
||||
if (expiryDates.length > 0) {
|
||||
earliestExpiryDate = new Date(Math.min(...expiryDates.map(d => d.getTime())));
|
||||
latestExpiryDate = new Date(Math.max(...expiryDates.map(d => d.getTime())));
|
||||
earliestExpiryDate = new Date(
|
||||
Math.min(...expiryDates.map((d) => d.getTime())),
|
||||
);
|
||||
latestExpiryDate = new Date(
|
||||
Math.max(...expiryDates.map((d) => d.getTime())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +287,7 @@ export class AdPositionService {
|
||||
earliestExpiryDate: earliestExpiryDate?.toISOString(),
|
||||
latestExpiryDate: latestExpiryDate?.toISOString(),
|
||||
categoryId,
|
||||
categoryName
|
||||
categoryName,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -258,27 +296,32 @@ export class AdPositionService {
|
||||
// Filter slots by category if specified and pageType is CATEGORY
|
||||
let filteredSlots = slots;
|
||||
if (category && pageType === PageType.CATEGORY) {
|
||||
filteredSlots = slots.filter(slot => slot.categoryId === category);
|
||||
filteredSlots = slots.filter((slot) => slot.categoryId === category);
|
||||
}
|
||||
|
||||
const totalSlots = filteredSlots.length;
|
||||
const occupiedSlots = filteredSlots.filter(slot => slot.isOccupied).length;
|
||||
const occupiedSlots = filteredSlots.filter(
|
||||
(slot) => slot.isOccupied,
|
||||
).length;
|
||||
const freeSlots = totalSlots - occupiedSlots;
|
||||
|
||||
return {
|
||||
totalSlots,
|
||||
occupiedSlots,
|
||||
freeSlots,
|
||||
slots: filteredSlots
|
||||
slots: filteredSlots,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line ads grouped by page type
|
||||
*/
|
||||
async getLineAds(pageType?: PageType, category?: string): Promise<LineAdsResponseDto> {
|
||||
async getLineAds(
|
||||
pageType?: PageType,
|
||||
category?: string,
|
||||
): Promise<LineAdsResponseDto> {
|
||||
const whereConditions: any = {
|
||||
adType: AdType.LINE
|
||||
adType: AdType.LINE,
|
||||
};
|
||||
|
||||
if (pageType) {
|
||||
@@ -290,21 +333,21 @@ export class AdPositionService {
|
||||
relations: ['lineAd', 'lineAd.customer', 'lineAd.customer.user'],
|
||||
order: {
|
||||
lineAd: {
|
||||
updated_at: 'DESC'
|
||||
}
|
||||
}
|
||||
updated_at: 'DESC',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const activeLineAds = lineAdPositions
|
||||
.filter(pos => pos.lineAd && this.isAdActive(pos.lineAd))
|
||||
.map(pos => pos.lineAd);
|
||||
.filter((pos) => pos.lineAd && this.isAdActive(pos.lineAd))
|
||||
.map((pos) => pos.lineAd);
|
||||
|
||||
const homeAds: LineAdSummaryDto[] = [];
|
||||
const categoryAds: LineAdSummaryDto[] = [];
|
||||
|
||||
for (const ad of activeLineAds) {
|
||||
const adPosition = await this.adPositionRepository.findOne({
|
||||
where: { lineAd: { id: ad.id } }
|
||||
where: { lineAd: { id: ad.id } },
|
||||
});
|
||||
|
||||
// For category pages, we'll need to get actual category data from the ad
|
||||
@@ -314,7 +357,8 @@ export class AdPositionService {
|
||||
|
||||
const summary: LineAdSummaryDto = {
|
||||
id: ad.id,
|
||||
title: ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''), // Use first 50 chars as title
|
||||
title:
|
||||
ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''), // Use first 50 chars as title
|
||||
content: ad.content,
|
||||
pageType: adPosition?.pageType || PageType.HOME,
|
||||
status: ad.status,
|
||||
@@ -322,7 +366,7 @@ export class AdPositionService {
|
||||
createdAt: ad.created_at,
|
||||
updatedAt: ad.updated_at,
|
||||
categoryId,
|
||||
categoryName
|
||||
categoryName,
|
||||
};
|
||||
|
||||
if (adPosition?.pageType === PageType.HOME) {
|
||||
@@ -335,22 +379,32 @@ export class AdPositionService {
|
||||
// Filter category ads by category if specified
|
||||
let filteredCategoryAds = categoryAds;
|
||||
if (category && pageType === PageType.CATEGORY) {
|
||||
filteredCategoryAds = categoryAds.filter(ad => ad.categoryId === category);
|
||||
filteredCategoryAds = categoryAds.filter(
|
||||
(ad) => ad.categoryId === category,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
homeAds,
|
||||
categoryAds: filteredCategoryAds,
|
||||
totalCount: homeAds.length + filteredCategoryAds.length
|
||||
totalCount: homeAds.length + filteredCategoryAds.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed information about a specific slot
|
||||
*/
|
||||
async getSlotDetails(pageType: PageType, side: PositionType, position: number, category?: string): Promise<SlotDetailsResponseDto> {
|
||||
async getSlotDetails(
|
||||
pageType: PageType,
|
||||
side: PositionType,
|
||||
position: number,
|
||||
category?: string,
|
||||
): Promise<SlotDetailsResponseDto> {
|
||||
const maxCapacity = 5;
|
||||
const actualPosition = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position;
|
||||
const actualPosition =
|
||||
side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM
|
||||
? 0
|
||||
: position;
|
||||
|
||||
// Get poster ads
|
||||
const posterAdPositions = await this.adPositionRepository.find({
|
||||
@@ -358,9 +412,9 @@ export class AdPositionService {
|
||||
pageType,
|
||||
side,
|
||||
position: actualPosition,
|
||||
adType: AdType.POSTER
|
||||
adType: AdType.POSTER,
|
||||
},
|
||||
relations: ['posterAd', 'posterAd.customer', 'posterAd.customer.user']
|
||||
relations: ['posterAd', 'posterAd.customer', 'posterAd.customer.user'],
|
||||
});
|
||||
|
||||
// Get video ads
|
||||
@@ -369,15 +423,15 @@ export class AdPositionService {
|
||||
pageType,
|
||||
side,
|
||||
position: actualPosition,
|
||||
adType: AdType.VIDEO
|
||||
adType: AdType.VIDEO,
|
||||
},
|
||||
relations: ['videoAd', 'videoAd.customer', 'videoAd.customer.user']
|
||||
relations: ['videoAd', 'videoAd.customer', 'videoAd.customer.user'],
|
||||
});
|
||||
|
||||
const allPositions = [...posterAdPositions, ...videoAdPositions];
|
||||
|
||||
|
||||
const ads: SlotAdDetailDto[] = allPositions
|
||||
.map(pos => {
|
||||
.map((pos) => {
|
||||
const ad = pos.posterAd || pos.videoAd;
|
||||
if (!ad) return null;
|
||||
|
||||
@@ -388,7 +442,9 @@ export class AdPositionService {
|
||||
|
||||
return {
|
||||
id: ad.id,
|
||||
title: pos.posterAd ? `Poster Ad by ${ad.postedBy}` : `Video Ad by ${ad.postedBy}`, // Create title from available data
|
||||
title: pos.posterAd
|
||||
? `Poster Ad by ${ad.postedBy}`
|
||||
: `Video Ad by ${ad.postedBy}`, // Create title from available data
|
||||
content: undefined, // Poster and Video ads don't have content field
|
||||
status: ad.status,
|
||||
isActive: this.isAdActive(ad),
|
||||
@@ -398,18 +454,18 @@ export class AdPositionService {
|
||||
customerName: ad.customer?.user?.name || 'Unknown',
|
||||
adType: pos.posterAd ? 'POSTER' : 'VIDEO',
|
||||
categoryId,
|
||||
categoryName
|
||||
categoryName,
|
||||
};
|
||||
})
|
||||
.filter(ad => ad !== null) as SlotAdDetailDto[];
|
||||
.filter((ad) => ad !== null) as SlotAdDetailDto[];
|
||||
|
||||
return {
|
||||
pageType,
|
||||
side,
|
||||
position,
|
||||
maxCapacity,
|
||||
currentOccupancy: ads.filter(ad => ad.isActive).length,
|
||||
ads
|
||||
currentOccupancy: ads.filter((ad) => ad.isActive).length,
|
||||
ads,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -421,23 +477,23 @@ export class AdPositionService {
|
||||
const [lineAds, posterAds, videoAds] = await Promise.all([
|
||||
this.lineAdRepository.find({
|
||||
where: { isActive: true, status: AdStatus.PUBLISHED },
|
||||
select: ['dates']
|
||||
select: ['dates'],
|
||||
}),
|
||||
this.posterAdRepository.find({
|
||||
where: { isActive: true, status: AdStatus.PUBLISHED },
|
||||
select: ['dates']
|
||||
select: ['dates'],
|
||||
}),
|
||||
this.videoAdRepository.find({
|
||||
where: { isActive: true, status: AdStatus.PUBLISHED },
|
||||
select: ['dates']
|
||||
})
|
||||
select: ['dates'],
|
||||
}),
|
||||
]);
|
||||
|
||||
const allDates = new Set<string>();
|
||||
|
||||
[...lineAds, ...posterAds, ...videoAds].forEach(ad => {
|
||||
|
||||
[...lineAds, ...posterAds, ...videoAds].forEach((ad) => {
|
||||
if (ad.dates && Array.isArray(ad.dates)) {
|
||||
ad.dates.forEach(dateStr => {
|
||||
ad.dates.forEach((dateStr) => {
|
||||
const dateOnly = dateStr.split('T')[0];
|
||||
allDates.add(dateOnly);
|
||||
});
|
||||
@@ -445,60 +501,95 @@ export class AdPositionService {
|
||||
});
|
||||
|
||||
const sortedDates = Array.from(allDates).sort();
|
||||
|
||||
|
||||
return {
|
||||
dates: sortedDates,
|
||||
totalDates: sortedDates.length
|
||||
totalDates: sortedDates.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ad slots overview for a specific date
|
||||
*/
|
||||
async getAdSlotsByDate(date: string, pageType?: PageType, category?: string): Promise<DateBasedAdSlotsOverviewDto> {
|
||||
async getAdSlotsByDate(
|
||||
date: string,
|
||||
pageType?: PageType,
|
||||
category?: string,
|
||||
): Promise<DateBasedAdSlotsOverviewDto> {
|
||||
const slots: DateBasedSlotOccupancyDto[] = [];
|
||||
const pageTypes = pageType ? [pageType] : [PageType.HOME, PageType.CATEGORY];
|
||||
const sides = [PositionType.LEFT_SIDE, PositionType.RIGHT_SIDE, PositionType.CENTER_TOP, PositionType.CENTER_BOTTOM];
|
||||
const pageTypes = pageType
|
||||
? [pageType]
|
||||
: [PageType.HOME, PageType.CATEGORY];
|
||||
const sides = [
|
||||
PositionType.LEFT_SIDE,
|
||||
PositionType.RIGHT_SIDE,
|
||||
PositionType.CENTER_TOP,
|
||||
PositionType.CENTER_BOTTOM,
|
||||
];
|
||||
const maxCapacity = 5;
|
||||
const allCategories = new Set<string>();
|
||||
|
||||
for (const page of pageTypes) {
|
||||
for (const side of sides) {
|
||||
const maxPositions = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 1 : 6;
|
||||
|
||||
const maxPositions =
|
||||
side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM
|
||||
? 1
|
||||
: 6;
|
||||
|
||||
for (let position = 1; position <= maxPositions; position++) {
|
||||
// Get all ads for this slot
|
||||
const adPositions = await this.adPositionRepository.find({
|
||||
where: {
|
||||
pageType: page,
|
||||
side,
|
||||
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
|
||||
adType: AdType.POSTER
|
||||
position:
|
||||
side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM
|
||||
? 0
|
||||
: position,
|
||||
adType: AdType.POSTER,
|
||||
},
|
||||
relations: ['posterAd', 'posterAd.mainCategory', 'posterAd.categoryOne', 'posterAd.categoryTwo', 'posterAd.categoryThree']
|
||||
relations: [
|
||||
'posterAd',
|
||||
'posterAd.mainCategory',
|
||||
'posterAd.categoryOne',
|
||||
'posterAd.categoryTwo',
|
||||
'posterAd.categoryThree',
|
||||
],
|
||||
});
|
||||
|
||||
const videoAdPositions = await this.adPositionRepository.find({
|
||||
where: {
|
||||
pageType: page,
|
||||
side,
|
||||
position: (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position,
|
||||
adType: AdType.VIDEO
|
||||
position:
|
||||
side === PositionType.CENTER_TOP ||
|
||||
side === PositionType.CENTER_BOTTOM
|
||||
? 0
|
||||
: position,
|
||||
adType: AdType.VIDEO,
|
||||
},
|
||||
relations: ['videoAd', 'videoAd.mainCategory', 'videoAd.categoryOne', 'videoAd.categoryTwo', 'videoAd.categoryThree']
|
||||
relations: [
|
||||
'videoAd',
|
||||
'videoAd.mainCategory',
|
||||
'videoAd.categoryOne',
|
||||
'videoAd.categoryTwo',
|
||||
'videoAd.categoryThree',
|
||||
],
|
||||
});
|
||||
|
||||
const allPositions = [...adPositions, ...videoAdPositions];
|
||||
|
||||
|
||||
// Filter for ads active on this specific date
|
||||
const activeAds = allPositions.filter(pos => {
|
||||
const activeAds = allPositions.filter((pos) => {
|
||||
const ad = pos.posterAd || pos.videoAd;
|
||||
return ad && this.isAdActiveForDate(ad, date);
|
||||
});
|
||||
|
||||
// Extract categories from active ads
|
||||
const slotCategories: CategoryDto[] = [];
|
||||
activeAds.forEach(pos => {
|
||||
activeAds.forEach((pos) => {
|
||||
const ad = pos.posterAd || pos.videoAd;
|
||||
if (ad && ad.mainCategory) {
|
||||
const categoryDto = this.categoryToDto(ad.mainCategory);
|
||||
@@ -509,7 +600,7 @@ export class AdPositionService {
|
||||
|
||||
// Filter by category if specified
|
||||
if (category && page === PageType.CATEGORY) {
|
||||
const hasCategory = activeAds.some(pos => {
|
||||
const hasCategory = activeAds.some((pos) => {
|
||||
const ad = pos.posterAd || pos.videoAd;
|
||||
return ad && ad.mainCategory && ad.mainCategory.id === category;
|
||||
});
|
||||
@@ -523,18 +614,20 @@ export class AdPositionService {
|
||||
activeAdsCount: activeAds.length,
|
||||
maxCapacity,
|
||||
isOccupied: activeAds.length > 0,
|
||||
categories: slotCategories
|
||||
categories: slotCategories,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalSlots = slots.length;
|
||||
const occupiedSlots = slots.filter(slot => slot.isOccupied).length;
|
||||
const occupiedSlots = slots.filter((slot) => slot.isOccupied).length;
|
||||
const freeSlots = totalSlots - occupiedSlots;
|
||||
|
||||
|
||||
// Convert Set back to array of CategoryDto
|
||||
const uniqueCategories = Array.from(allCategories).map(catStr => JSON.parse(catStr) as CategoryDto);
|
||||
const uniqueCategories = Array.from(allCategories).map(
|
||||
(catStr) => JSON.parse(catStr) as CategoryDto,
|
||||
);
|
||||
|
||||
return {
|
||||
date,
|
||||
@@ -542,16 +635,20 @@ export class AdPositionService {
|
||||
occupiedSlots,
|
||||
freeSlots,
|
||||
slots,
|
||||
categories: uniqueCategories
|
||||
categories: uniqueCategories,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line ads for a specific date
|
||||
*/
|
||||
async getLineAdsByDate(date: string, pageType?: PageType, category?: string): Promise<DateBasedLineAdsDto> {
|
||||
async getLineAdsByDate(
|
||||
date: string,
|
||||
pageType?: PageType,
|
||||
category?: string,
|
||||
): Promise<DateBasedLineAdsDto> {
|
||||
const whereConditions: any = {
|
||||
adType: AdType.LINE
|
||||
adType: AdType.LINE,
|
||||
};
|
||||
|
||||
if (pageType) {
|
||||
@@ -567,18 +664,18 @@ export class AdPositionService {
|
||||
'lineAd.mainCategory',
|
||||
'lineAd.categoryOne',
|
||||
'lineAd.categoryTwo',
|
||||
'lineAd.categoryThree'
|
||||
'lineAd.categoryThree',
|
||||
],
|
||||
order: {
|
||||
lineAd: {
|
||||
updated_at: 'DESC'
|
||||
}
|
||||
}
|
||||
updated_at: 'DESC',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const activeLineAds = lineAdPositions
|
||||
.filter(pos => pos.lineAd && this.isAdActiveForDate(pos.lineAd, date))
|
||||
.map(pos => ({ ad: pos.lineAd, pageType: pos.pageType }));
|
||||
.filter((pos) => pos.lineAd && this.isAdActiveForDate(pos.lineAd, date))
|
||||
.map((pos) => ({ ad: pos.lineAd, pageType: pos.pageType }));
|
||||
|
||||
const homeAds: DateBasedLineAdDto[] = [];
|
||||
const categoryAds: DateBasedLineAdDto[] = [];
|
||||
@@ -587,16 +684,25 @@ export class AdPositionService {
|
||||
for (const { ad, pageType: adPageType } of activeLineAds) {
|
||||
const adDto: DateBasedLineAdDto = {
|
||||
id: ad.id,
|
||||
title: ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''),
|
||||
title:
|
||||
ad.content.substring(0, 50) + (ad.content.length > 50 ? '...' : ''),
|
||||
content: ad.content,
|
||||
pageType: adPageType || PageType.HOME,
|
||||
status: ad.status,
|
||||
createdAt: ad.created_at,
|
||||
updatedAt: ad.updated_at,
|
||||
mainCategory: ad.mainCategory ? this.categoryToDto(ad.mainCategory) : undefined,
|
||||
categoryOne: ad.categoryOne ? { id: ad.categoryOne.id, name: ad.categoryOne.name } : undefined,
|
||||
categoryTwo: ad.categoryTwo ? { id: ad.categoryTwo.id, name: ad.categoryTwo.name } : undefined,
|
||||
categoryThree: ad.categoryThree ? { id: ad.categoryThree.id, name: ad.categoryThree.name } : undefined
|
||||
mainCategory: ad.mainCategory
|
||||
? this.categoryToDto(ad.mainCategory)
|
||||
: undefined,
|
||||
categoryOne: ad.categoryOne
|
||||
? { id: ad.categoryOne.id, name: ad.categoryOne.name }
|
||||
: undefined,
|
||||
categoryTwo: ad.categoryTwo
|
||||
? { id: ad.categoryTwo.id, name: ad.categoryTwo.name }
|
||||
: undefined,
|
||||
categoryThree: ad.categoryThree
|
||||
? { id: ad.categoryThree.id, name: ad.categoryThree.name }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Track categories
|
||||
@@ -605,7 +711,11 @@ export class AdPositionService {
|
||||
}
|
||||
|
||||
// Filter by category if specified
|
||||
if (category && adPageType === PageType.CATEGORY && (!ad.mainCategory || ad.mainCategory.id !== category)) {
|
||||
if (
|
||||
category &&
|
||||
adPageType === PageType.CATEGORY &&
|
||||
(!ad.mainCategory || ad.mainCategory.id !== category)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -616,14 +726,16 @@ export class AdPositionService {
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueCategories = Array.from(allCategories).map(catStr => JSON.parse(catStr) as CategoryDto);
|
||||
const uniqueCategories = Array.from(allCategories).map(
|
||||
(catStr) => JSON.parse(catStr) as CategoryDto,
|
||||
);
|
||||
|
||||
return {
|
||||
date,
|
||||
homeAds,
|
||||
categoryAds,
|
||||
totalCount: homeAds.length + categoryAds.length,
|
||||
categories: uniqueCategories
|
||||
categories: uniqueCategories,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -635,10 +747,13 @@ export class AdPositionService {
|
||||
pageType: PageType,
|
||||
side: PositionType,
|
||||
position: number,
|
||||
category?: string
|
||||
category?: string,
|
||||
): Promise<DateBasedSlotDetailsDto> {
|
||||
const maxCapacity = 5;
|
||||
const actualPosition = (side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM) ? 0 : position;
|
||||
const actualPosition =
|
||||
side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM
|
||||
? 0
|
||||
: position;
|
||||
|
||||
// Get poster ads
|
||||
const posterAdPositions = await this.adPositionRepository.find({
|
||||
@@ -646,7 +761,7 @@ export class AdPositionService {
|
||||
pageType,
|
||||
side,
|
||||
position: actualPosition,
|
||||
adType: AdType.POSTER
|
||||
adType: AdType.POSTER,
|
||||
},
|
||||
relations: [
|
||||
'posterAd',
|
||||
@@ -655,8 +770,8 @@ export class AdPositionService {
|
||||
'posterAd.mainCategory',
|
||||
'posterAd.categoryOne',
|
||||
'posterAd.categoryTwo',
|
||||
'posterAd.categoryThree'
|
||||
]
|
||||
'posterAd.categoryThree',
|
||||
],
|
||||
});
|
||||
|
||||
// Get video ads
|
||||
@@ -665,7 +780,7 @@ export class AdPositionService {
|
||||
pageType,
|
||||
side,
|
||||
position: actualPosition,
|
||||
adType: AdType.VIDEO
|
||||
adType: AdType.VIDEO,
|
||||
},
|
||||
relations: [
|
||||
'videoAd',
|
||||
@@ -674,33 +789,41 @@ export class AdPositionService {
|
||||
'videoAd.mainCategory',
|
||||
'videoAd.categoryOne',
|
||||
'videoAd.categoryTwo',
|
||||
'videoAd.categoryThree'
|
||||
]
|
||||
'videoAd.categoryThree',
|
||||
],
|
||||
});
|
||||
|
||||
const allPositions = [...posterAdPositions, ...videoAdPositions];
|
||||
const allCategories = new Set<string>();
|
||||
|
||||
|
||||
const ads: DateBasedSlotAdDetailDto[] = allPositions
|
||||
.map(pos => {
|
||||
.map((pos) => {
|
||||
const ad = pos.posterAd || pos.videoAd;
|
||||
if (!ad) return null;
|
||||
|
||||
const isActive = this.isAdActiveForDate(ad, date);
|
||||
|
||||
|
||||
// Track categories
|
||||
if (ad.mainCategory) {
|
||||
allCategories.add(JSON.stringify(this.categoryToDto(ad.mainCategory)));
|
||||
allCategories.add(
|
||||
JSON.stringify(this.categoryToDto(ad.mainCategory)),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by category if specified
|
||||
if (category && pageType === PageType.CATEGORY && (!ad.mainCategory || ad.mainCategory.id !== category)) {
|
||||
if (
|
||||
category &&
|
||||
pageType === PageType.CATEGORY &&
|
||||
(!ad.mainCategory || ad.mainCategory.id !== category)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: ad.id,
|
||||
title: pos.posterAd ? `Poster Ad by ${ad.postedBy}` : `Video Ad by ${ad.postedBy}`,
|
||||
title: pos.posterAd
|
||||
? `Poster Ad by ${ad.postedBy}`
|
||||
: `Video Ad by ${ad.postedBy}`,
|
||||
content: undefined,
|
||||
status: ad.status,
|
||||
isActive,
|
||||
@@ -708,15 +831,25 @@ export class AdPositionService {
|
||||
updatedAt: ad.updated_at,
|
||||
customerName: ad.customer?.user?.name || 'Unknown',
|
||||
adType: pos.posterAd ? 'POSTER' : 'VIDEO',
|
||||
mainCategory: ad.mainCategory ? this.categoryToDto(ad.mainCategory) : undefined,
|
||||
categoryOne: ad.categoryOne ? { id: ad.categoryOne.id, name: ad.categoryOne.name } : undefined,
|
||||
categoryTwo: ad.categoryTwo ? { id: ad.categoryTwo.id, name: ad.categoryTwo.name } : undefined,
|
||||
categoryThree: ad.categoryThree ? { id: ad.categoryThree.id, name: ad.categoryThree.name } : undefined
|
||||
mainCategory: ad.mainCategory
|
||||
? this.categoryToDto(ad.mainCategory)
|
||||
: undefined,
|
||||
categoryOne: ad.categoryOne
|
||||
? { id: ad.categoryOne.id, name: ad.categoryOne.name }
|
||||
: undefined,
|
||||
categoryTwo: ad.categoryTwo
|
||||
? { id: ad.categoryTwo.id, name: ad.categoryTwo.name }
|
||||
: undefined,
|
||||
categoryThree: ad.categoryThree
|
||||
? { id: ad.categoryThree.id, name: ad.categoryThree.name }
|
||||
: undefined,
|
||||
};
|
||||
})
|
||||
.filter(ad => ad !== null) as DateBasedSlotAdDetailDto[];
|
||||
.filter((ad) => ad !== null) as DateBasedSlotAdDetailDto[];
|
||||
|
||||
const uniqueCategories = Array.from(allCategories).map(catStr => JSON.parse(catStr) as CategoryDto);
|
||||
const uniqueCategories = Array.from(allCategories).map(
|
||||
(catStr) => JSON.parse(catStr) as CategoryDto,
|
||||
);
|
||||
|
||||
return {
|
||||
date,
|
||||
@@ -724,9 +857,9 @@ export class AdPositionService {
|
||||
side,
|
||||
position,
|
||||
maxCapacity,
|
||||
currentOccupancy: ads.filter(ad => ad.isActive).length,
|
||||
currentOccupancy: ads.filter((ad) => ad.isActive).length,
|
||||
ads,
|
||||
categories: uniqueCategories
|
||||
categories: uniqueCategories,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,4 @@ export class SlotDetailsResponseDto {
|
||||
maxCapacity: number;
|
||||
currentOccupancy: number;
|
||||
ads: SlotAdDetailDto[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,4 +79,4 @@ export class DateBasedSlotDetailsDto {
|
||||
currentOccupancy: number;
|
||||
ads: DateBasedSlotAdDetailDto[];
|
||||
categories: CategoryDto[]; // All unique categories in this slot
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
UseGuards,
|
||||
Req,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { Response, Request } from 'express';
|
||||
@@ -16,8 +18,6 @@ import { Role } from 'src/common/enums/role.enum';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
private static DOMAIN = 'paisaads-v2.anujs.dev';
|
||||
// private static DOMAIN = 'localhost';
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@@ -80,4 +80,86 @@ export class AuthController {
|
||||
});
|
||||
return { message: 'Viewer login successful', user };
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('send-otp')
|
||||
async sendOtp(@Body('phone') phone: string) {
|
||||
if (!phone) {
|
||||
throw new BadRequestException('Phone number is required');
|
||||
}
|
||||
return this.authService.generateOtp(phone);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('verify-otp')
|
||||
async verifyOtp(
|
||||
@Body('phone') phone: string,
|
||||
@Body('otp') otp: string,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
if (!phone || !otp) {
|
||||
throw new BadRequestException('Phone number and OTP are required');
|
||||
}
|
||||
|
||||
const result = await this.authService.verifyOtp(phone, otp);
|
||||
|
||||
if (result.token) {
|
||||
res.cookie('token', result.token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
secure: true,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
return { message: result.message };
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('send-verification-otp')
|
||||
async sendVerificationOtp(
|
||||
@Body('emailOrPhone') emailOrPhone: string,
|
||||
@Body('type') type: 'email' | 'phone',
|
||||
) {
|
||||
if (!emailOrPhone || !type) {
|
||||
throw new BadRequestException('Email/Phone and type are required');
|
||||
}
|
||||
|
||||
if (type !== 'email' && type !== 'phone') {
|
||||
throw new BadRequestException('Type must be either "email" or "phone"');
|
||||
}
|
||||
|
||||
return this.authService.sendVerificationOtp(emailOrPhone, type);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('verify-account')
|
||||
async verifyAccount(
|
||||
@Body('emailOrPhone') emailOrPhone: string,
|
||||
@Body('otp') otp: string,
|
||||
@Body('type') type: 'email' | 'phone',
|
||||
) {
|
||||
if (!emailOrPhone || !otp || !type) {
|
||||
throw new BadRequestException('Email/Phone, OTP, and type are required');
|
||||
}
|
||||
|
||||
if (type !== 'email' && type !== 'phone') {
|
||||
throw new BadRequestException('Type must be either "email" or "phone"');
|
||||
}
|
||||
|
||||
return this.authService.verifyAccount(emailOrPhone, otp, type);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('auto-verify-phone')
|
||||
async autoVerifyPhone(
|
||||
@Body('phone') phone: string,
|
||||
@Body('otp') otp: string,
|
||||
) {
|
||||
if (!phone || !otp) {
|
||||
throw new BadRequestException('Phone number and OTP are required');
|
||||
}
|
||||
|
||||
return this.authService.autoVerifyPhone(phone, otp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from './guard/jwt.guard';
|
||||
import { RolesGuard } from './guard/role.guard';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Otp } from './entities/otp.entity';
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
PassportModule,
|
||||
TypeOrmModule.forFeature([Otp]),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
@@ -25,6 +29,7 @@ import { RolesGuard } from './guard/role.guard';
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
SmsService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UserService } from '../user/user.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { User } from 'src/user/entities/user.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, MoreThan } from 'typeorm';
|
||||
import { Otp } from './entities/otp.entity';
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly jwtService: JwtService,
|
||||
@InjectRepository(Otp)
|
||||
private readonly otpRepository: Repository<Otp>,
|
||||
private readonly smsService: SmsService,
|
||||
) {}
|
||||
|
||||
async validateUser(emailOrPhone: string, password: string) {
|
||||
const user =
|
||||
await this.userService.findByPhoneOrEmailWithPassword(emailOrPhone);
|
||||
if (user && (await bcrypt.compare(password, user.password))) {
|
||||
// Check if user account is verified (only for USER role)
|
||||
if (user.role === 'USER') {
|
||||
if (!user.phone_verified) {
|
||||
throw new UnauthorizedException('Please verify your phone number.');
|
||||
}
|
||||
// if (!user.email_verified) {
|
||||
// throw new UnauthorizedException('Please verify your email address.');
|
||||
// }
|
||||
}
|
||||
|
||||
const { password, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
@@ -40,4 +62,262 @@ export class AuthService {
|
||||
async getUserProfile(userId: string): Promise<User> {
|
||||
return this.userService.findOneById(userId);
|
||||
}
|
||||
|
||||
async generateOtp(phone: string): Promise<{ message: string }> {
|
||||
// Check if user exists and their role
|
||||
const user = await this.userService.findByPhone(phone);
|
||||
|
||||
// If user is admin, reject OTP request
|
||||
if (
|
||||
user &&
|
||||
(user.role === 'SUPER_ADMIN' ||
|
||||
user.role === 'EDITOR' ||
|
||||
user.role === 'REVIEWER' ||
|
||||
user.role === 'VIEWER')
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Admin users should use regular login, not OTP',
|
||||
);
|
||||
}
|
||||
|
||||
// If user exists and has an account, they should verify first or use password login
|
||||
if (user && user.role === 'USER') {
|
||||
if (!user.phone_verified) {
|
||||
throw new BadRequestException(
|
||||
'Please verify your phone number first.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivate any existing active LOGIN OTPs for this phone number
|
||||
await this.otpRepository.update(
|
||||
{ phone_number: phone, purpose: 'LOGIN', isActive: true },
|
||||
{ isActive: false },
|
||||
);
|
||||
|
||||
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + 5);
|
||||
|
||||
const otp = this.otpRepository.create({
|
||||
phone_number: phone,
|
||||
otp_code: otpCode,
|
||||
expires_at: expiresAt,
|
||||
is_verified: false,
|
||||
isActive: true,
|
||||
purpose: 'LOGIN',
|
||||
user: user ?? undefined, // Link to user if exists
|
||||
});
|
||||
|
||||
await this.otpRepository.save(otp);
|
||||
|
||||
// Send SMS using the SMS service
|
||||
const smsSent = await this.smsService.sendOtp(phone, otpCode);
|
||||
|
||||
if (!smsSent) {
|
||||
// If SMS fails, still log to console as fallback for development
|
||||
console.log(`SMS failed, OTP for ${phone}: ${otpCode}`);
|
||||
return { message: 'OTP generated but SMS delivery may have failed' };
|
||||
}
|
||||
|
||||
return { message: 'OTP sent successfully' };
|
||||
}
|
||||
|
||||
async verifyOtp(
|
||||
phone: string,
|
||||
otpCode: string,
|
||||
): Promise<{ message: string; token?: string }> {
|
||||
const otp = await this.otpRepository.findOne({
|
||||
where: {
|
||||
phone_number: phone,
|
||||
otp_code: otpCode,
|
||||
purpose: 'LOGIN',
|
||||
isActive: true,
|
||||
is_verified: false,
|
||||
expires_at: MoreThan(new Date()),
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (!otp) {
|
||||
throw new BadRequestException('Invalid or expired OTP');
|
||||
}
|
||||
|
||||
await this.otpRepository.update(otp.id, {
|
||||
is_verified: true,
|
||||
isActive: false,
|
||||
});
|
||||
|
||||
// If linked to a real user, log them in with their actual data
|
||||
// Otherwise, create a viewer session
|
||||
let user;
|
||||
if (otp.user) {
|
||||
user = {
|
||||
id: otp.user.id,
|
||||
name: otp.user.name,
|
||||
phone_number: otp.user.phone_number,
|
||||
role: otp.user.role,
|
||||
};
|
||||
} else {
|
||||
user = { id: null, name: null, phone_number: phone, role: 'VIEWER' };
|
||||
}
|
||||
|
||||
const token = await this.login(user);
|
||||
|
||||
return { message: 'OTP verified successfully', token };
|
||||
}
|
||||
|
||||
async sendVerificationOtp(
|
||||
emailOrPhone: string,
|
||||
type: 'email' | 'phone',
|
||||
): Promise<{ message: string }> {
|
||||
const user =
|
||||
await this.userService.findByPhoneOrEmailWithPassword(emailOrPhone);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
|
||||
if (type === 'phone' && user.phone_verified) {
|
||||
throw new BadRequestException('Phone number is already verified');
|
||||
}
|
||||
|
||||
if (type === 'email' && user.email_verified) {
|
||||
throw new BadRequestException('Email is already verified');
|
||||
}
|
||||
|
||||
const purpose =
|
||||
type === 'email' ? 'EMAIL_VERIFICATION' : 'PHONE_VERIFICATION';
|
||||
const contact = type === 'email' ? user.email : user.phone_number;
|
||||
|
||||
// Deactivate any existing verification OTPs
|
||||
await this.otpRepository.update(
|
||||
{ phone_number: contact, purpose, isActive: true },
|
||||
{ isActive: false },
|
||||
);
|
||||
|
||||
const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + 10); // 10 minutes for verification
|
||||
|
||||
const otp = this.otpRepository.create({
|
||||
phone_number: contact,
|
||||
otp_code: otpCode,
|
||||
expires_at: expiresAt,
|
||||
is_verified: false,
|
||||
isActive: true,
|
||||
purpose,
|
||||
user,
|
||||
});
|
||||
|
||||
await this.otpRepository.save(otp);
|
||||
|
||||
if (type === 'phone') {
|
||||
const smsSent = await this.smsService.sendOtp(contact, otpCode);
|
||||
if (!smsSent) {
|
||||
console.log(`SMS failed, Verification OTP for ${contact}: ${otpCode}`);
|
||||
return {
|
||||
message:
|
||||
'Verification OTP generated but SMS delivery may have failed',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// For email, log to console (you can implement email service later)
|
||||
console.log(`Email verification OTP for ${contact}: ${otpCode}`);
|
||||
}
|
||||
|
||||
return { message: `Verification OTP sent to your ${type}` };
|
||||
}
|
||||
|
||||
async verifyAccount(
|
||||
emailOrPhone: string,
|
||||
otpCode: string,
|
||||
type: 'email' | 'phone',
|
||||
): Promise<{ message: string }> {
|
||||
const purpose =
|
||||
type === 'email' ? 'EMAIL_VERIFICATION' : 'PHONE_VERIFICATION';
|
||||
|
||||
const otp = await this.otpRepository.findOne({
|
||||
where: {
|
||||
phone_number: emailOrPhone,
|
||||
otp_code: otpCode,
|
||||
purpose,
|
||||
isActive: true,
|
||||
is_verified: false,
|
||||
expires_at: MoreThan(new Date()),
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (!otp || !otp.user) {
|
||||
throw new BadRequestException('Invalid or expired verification OTP');
|
||||
}
|
||||
|
||||
// Mark OTP as used
|
||||
await this.otpRepository.update(otp.id, {
|
||||
is_verified: true,
|
||||
isActive: false,
|
||||
});
|
||||
|
||||
// Update user verification status
|
||||
const updateData =
|
||||
type === 'email' ? { email_verified: true } : { phone_verified: true };
|
||||
|
||||
await this.userService.updateUser(otp.user.id, updateData);
|
||||
|
||||
return {
|
||||
message: `${type === 'email' ? 'Email' : 'Phone number'} verified successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
async autoVerifyPhone(phone: string, otpCode: string): Promise<{ message: string; user?: any }> {
|
||||
const user = await this.userService.findByPhone(phone);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('No account found with this phone number');
|
||||
}
|
||||
|
||||
if (user.phone_verified) {
|
||||
throw new BadRequestException('Phone number already verified');
|
||||
}
|
||||
|
||||
// Find the OTP
|
||||
const otp = await this.otpRepository.findOne({
|
||||
where: {
|
||||
phone_number: phone,
|
||||
otp_code: otpCode,
|
||||
purpose: 'PHONE_VERIFICATION',
|
||||
isActive: true,
|
||||
is_verified: false,
|
||||
expires_at: MoreThan(new Date()),
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (!otp) {
|
||||
throw new BadRequestException('Invalid or expired OTP');
|
||||
}
|
||||
|
||||
// Mark OTP as used
|
||||
await this.otpRepository.update(otp.id, {
|
||||
is_verified: true,
|
||||
isActive: false,
|
||||
});
|
||||
|
||||
// Update user phone verification status
|
||||
await this.userService.updateUser(user.id, { phone_verified: true });
|
||||
|
||||
return {
|
||||
message: 'Phone number verified successfully',
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
phone_number: user.phone_number,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
phone_verified: true,
|
||||
email_verified: user.email_verified,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
28
src/auth/entities/otp.entity.ts
Normal file
28
src/auth/entities/otp.entity.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { BaseEntity } from 'src/common/types/base-entity';
|
||||
import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { User } from 'src/user/entities/user.entity';
|
||||
|
||||
@Entity('otps')
|
||||
export class Otp extends BaseEntity {
|
||||
@Column()
|
||||
phone_number: string;
|
||||
|
||||
@Column()
|
||||
otp_code: string;
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
expires_at: Date;
|
||||
|
||||
@Column({ default: false })
|
||||
is_verified: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ default: 'LOGIN' })
|
||||
purpose: string; // 'LOGIN', 'EMAIL_VERIFICATION', 'PHONE_VERIFICATION'
|
||||
|
||||
@ManyToOne(() => User, (user) => user.otps, { nullable: true })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
}
|
||||
39
src/auth/sms.service.ts
Normal file
39
src/auth/sms.service.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
private readonly logger = new Logger(SmsService.name);
|
||||
private readonly SMS_API_URL = 'http://bhashsms.com/api/sendmsg.php';
|
||||
private readonly SMS_USER = 'paisaads';
|
||||
private readonly SMS_PASS = '123456';
|
||||
private readonly SMS_SENDER = 'PSADDS';
|
||||
|
||||
async sendOtp(phoneNumber: string, otp: string): Promise<boolean> {
|
||||
try {
|
||||
const message = `Your One Time Password (OTP) for login is ${otp}. Please do not share the OTP with anyone. Best Regards, www.PaisaAds.com - contact@PaisaAds.com`;
|
||||
|
||||
const params = {
|
||||
user: this.SMS_USER,
|
||||
pass: this.SMS_PASS,
|
||||
sender: this.SMS_SENDER,
|
||||
phone: phoneNumber,
|
||||
text: message,
|
||||
priority: 'ndnd',
|
||||
stype: 'normal',
|
||||
};
|
||||
|
||||
this.logger.log(`Sending OTP to ${phoneNumber}`);
|
||||
|
||||
const response = await axios.get(this.SMS_API_URL, { params });
|
||||
|
||||
this.logger.log(`SMS API Response: ${response.data}`);
|
||||
|
||||
// Consider successful if status is 200
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send SMS to ${phoneNumber}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ export class CategoryThree extends BaseEntity {
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@ManyToOne(() => CategoryTwo, (catTwo) => catTwo.subCategories, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => CategoryTwo, (catTwo) => catTwo.subCategories, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
parent: CategoryTwo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import { Body, Controller, Get, Post, Param, Patch, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
Patch,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { ConfigurationsService } from './configurations.service';
|
||||
import { TermsAndConditionsDto } from './dto/terms-and-conditions';
|
||||
import { AdPricingDto } from './dto/ad-pricing.dto';
|
||||
@@ -21,7 +35,10 @@ export class ConfigurationsController {
|
||||
@Post('terms-and-conditions')
|
||||
@Roles(Role.SUPER_ADMIN)
|
||||
@ApiOperation({ summary: 'Create or update terms and conditions' })
|
||||
@ApiResponse({ status: 201, description: 'Terms and conditions updated successfully' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Terms and conditions updated successfully',
|
||||
})
|
||||
async createTermsAndConditions(
|
||||
@Body() termsAndConditionsDto: TermsAndConditionsDto,
|
||||
) {
|
||||
@@ -33,7 +50,10 @@ export class ConfigurationsController {
|
||||
@Get('terms-and-conditions')
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get current terms and conditions' })
|
||||
@ApiResponse({ status: 200, description: 'Terms and conditions retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Terms and conditions retrieved successfully',
|
||||
})
|
||||
async getTermsAndConditions() {
|
||||
return this.configurationsService.getTermsAndConditions();
|
||||
}
|
||||
@@ -51,7 +71,10 @@ export class ConfigurationsController {
|
||||
@Get('ad-pricing')
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get current ad pricing' })
|
||||
@ApiResponse({ status: 200, description: 'Ad pricing retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Ad pricing retrieved successfully',
|
||||
})
|
||||
async getAdPricing() {
|
||||
return this.configurationsService.getAdPricing();
|
||||
}
|
||||
@@ -59,7 +82,10 @@ export class ConfigurationsController {
|
||||
@Get('ad-pricing/history')
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
|
||||
@ApiOperation({ summary: 'Get ad pricing history' })
|
||||
@ApiResponse({ status: 200, description: 'Ad pricing history retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Ad pricing history retrieved successfully',
|
||||
})
|
||||
async getAdPricingHistory() {
|
||||
return this.configurationsService.getAdPricingHistory();
|
||||
}
|
||||
@@ -69,15 +95,25 @@ export class ConfigurationsController {
|
||||
@Post('privacy-policy')
|
||||
@Roles(Role.SUPER_ADMIN)
|
||||
@ApiOperation({ summary: 'Create or update privacy policy' })
|
||||
@ApiResponse({ status: 201, description: 'Privacy policy updated successfully' })
|
||||
async createOrUpdatePrivacyPolicy(@Body() privacyPolicyDto: PrivacyPolicyDto) {
|
||||
return this.configurationsService.createOrUpdatePrivacyPolicy(privacyPolicyDto);
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Privacy policy updated successfully',
|
||||
})
|
||||
async createOrUpdatePrivacyPolicy(
|
||||
@Body() privacyPolicyDto: PrivacyPolicyDto,
|
||||
) {
|
||||
return this.configurationsService.createOrUpdatePrivacyPolicy(
|
||||
privacyPolicyDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('privacy-policy')
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get current privacy policy' })
|
||||
@ApiResponse({ status: 200, description: 'Privacy policy retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Privacy policy retrieved successfully',
|
||||
})
|
||||
async getPrivacyPolicy() {
|
||||
return this.configurationsService.getPrivacyPolicy();
|
||||
}
|
||||
@@ -85,7 +121,10 @@ export class ConfigurationsController {
|
||||
@Get('privacy-policy/history')
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
|
||||
@ApiOperation({ summary: 'Get privacy policy history' })
|
||||
@ApiResponse({ status: 200, description: 'Privacy policy history retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Privacy policy history retrieved successfully',
|
||||
})
|
||||
async getPrivacyPolicyHistory() {
|
||||
return this.configurationsService.getPrivacyPolicyHistory();
|
||||
}
|
||||
@@ -95,15 +134,23 @@ export class ConfigurationsController {
|
||||
@Post('search-slogan')
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
|
||||
@ApiOperation({ summary: 'Create or update search page slogan' })
|
||||
@ApiResponse({ status: 201, description: 'Search slogan updated successfully' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Search slogan updated successfully',
|
||||
})
|
||||
async createOrUpdateSearchSlogan(@Body() searchSloganDto: SearchSloganDto) {
|
||||
return this.configurationsService.createOrUpdateSearchSlogan(searchSloganDto);
|
||||
return this.configurationsService.createOrUpdateSearchSlogan(
|
||||
searchSloganDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('search-slogan')
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get current search page slogan' })
|
||||
@ApiResponse({ status: 200, description: 'Search slogan retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Search slogan retrieved successfully',
|
||||
})
|
||||
async getSearchSlogan() {
|
||||
return this.configurationsService.getSearchSlogan();
|
||||
}
|
||||
@@ -113,7 +160,10 @@ export class ConfigurationsController {
|
||||
@Post('about-us')
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
|
||||
@ApiOperation({ summary: 'Create or update about us page' })
|
||||
@ApiResponse({ status: 201, description: 'About us page updated successfully' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'About us page updated successfully',
|
||||
})
|
||||
async createOrUpdateAboutUs(@Body() aboutUsDto: AboutUsDto) {
|
||||
return this.configurationsService.createOrUpdateAboutUs(aboutUsDto);
|
||||
}
|
||||
@@ -121,7 +171,10 @@ export class ConfigurationsController {
|
||||
@Get('about-us')
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get current about us page content' })
|
||||
@ApiResponse({ status: 200, description: 'About us content retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'About us content retrieved successfully',
|
||||
})
|
||||
async getAboutUs() {
|
||||
return this.configurationsService.getAboutUs();
|
||||
}
|
||||
@@ -148,7 +201,10 @@ export class ConfigurationsController {
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get FAQ by category' })
|
||||
@ApiParam({ name: 'category', description: 'FAQ category' })
|
||||
@ApiResponse({ status: 200, description: 'FAQ category retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'FAQ category retrieved successfully',
|
||||
})
|
||||
async getFaqByCategory(@Param('category') category: string) {
|
||||
return this.configurationsService.getFaqByCategory(category);
|
||||
}
|
||||
@@ -158,12 +214,12 @@ export class ConfigurationsController {
|
||||
@ApiOperation({ summary: 'Add a new FAQ question' })
|
||||
@ApiResponse({ status: 201, description: 'FAQ question added successfully' })
|
||||
async addFaqQuestion(
|
||||
@Body() body: { question: string; answer: string; category: string }
|
||||
@Body() body: { question: string; answer: string; category: string },
|
||||
) {
|
||||
return this.configurationsService.addFaqQuestion(
|
||||
body.question,
|
||||
body.answer,
|
||||
body.category
|
||||
body.category,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,20 +227,24 @@ export class ConfigurationsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
|
||||
@ApiOperation({ summary: 'Update an existing FAQ question' })
|
||||
@ApiParam({ name: 'index', description: 'Question index' })
|
||||
@ApiResponse({ status: 200, description: 'FAQ question updated successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'FAQ question updated successfully',
|
||||
})
|
||||
async updateFaqQuestion(
|
||||
@Param('index') index: string,
|
||||
@Body() updatedQuestion: Partial<{
|
||||
@Body()
|
||||
updatedQuestion: Partial<{
|
||||
question: string;
|
||||
answer: string;
|
||||
category: string;
|
||||
order: number;
|
||||
isActive: boolean;
|
||||
}>
|
||||
}>,
|
||||
) {
|
||||
return this.configurationsService.updateFaqQuestion(
|
||||
parseInt(index),
|
||||
updatedQuestion
|
||||
updatedQuestion,
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
@@ -193,7 +253,10 @@ export class ConfigurationsController {
|
||||
@Post('contact-page')
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR)
|
||||
@ApiOperation({ summary: 'Create or update contact page' })
|
||||
@ApiResponse({ status: 201, description: 'Contact page updated successfully' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Contact page updated successfully',
|
||||
})
|
||||
async createOrUpdateContactPage(@Body() contactPageDto: ContactPageDto) {
|
||||
return this.configurationsService.createOrUpdateContactPage(contactPageDto);
|
||||
}
|
||||
@@ -201,7 +264,10 @@ export class ConfigurationsController {
|
||||
@Get('contact-page')
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get contact page information' })
|
||||
@ApiResponse({ status: 200, description: 'Contact page retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Contact page retrieved successfully',
|
||||
})
|
||||
async getContactPage() {
|
||||
return this.configurationsService.getContactPage();
|
||||
}
|
||||
@@ -211,7 +277,10 @@ export class ConfigurationsController {
|
||||
@Get('all')
|
||||
@Public()
|
||||
@ApiOperation({ summary: 'Get all configurations' })
|
||||
@ApiResponse({ status: 200, description: 'All configurations retrieved successfully' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'All configurations retrieved successfully',
|
||||
})
|
||||
async getAllConfigurations() {
|
||||
return this.configurationsService.getAllConfigurations();
|
||||
}
|
||||
|
||||
@@ -7,8 +7,14 @@ import {
|
||||
TermsAndConditionsSchema,
|
||||
} from './schemas/terms-and-conditions.schema';
|
||||
import { AdPricing, AdPricingSchema } from './schemas/ad-pricing.schema';
|
||||
import { PrivacyPolicy, PrivacyPolicySchema } from './schemas/privacy-policy.schema';
|
||||
import { SearchSlogan, SearchSloganSchema } from './schemas/search-slogan.schema';
|
||||
import {
|
||||
PrivacyPolicy,
|
||||
PrivacyPolicySchema,
|
||||
} from './schemas/privacy-policy.schema';
|
||||
import {
|
||||
SearchSlogan,
|
||||
SearchSloganSchema,
|
||||
} from './schemas/search-slogan.schema';
|
||||
import { AboutUs, AboutUsSchema } from './schemas/about-us.schema';
|
||||
import { Faq, FaqSchema } from './schemas/faq.schema';
|
||||
import { ContactPage, ContactPageSchema } from './schemas/contact-page.schema';
|
||||
|
||||
@@ -79,7 +79,9 @@ export class ConfigurationsService {
|
||||
}
|
||||
|
||||
async getAdPricingHistory() {
|
||||
const pricingHistory = await this.adPricingModel.find().sort({ lastUpdated: -1 });
|
||||
const pricingHistory = await this.adPricingModel
|
||||
.find()
|
||||
.sort({ lastUpdated: -1 });
|
||||
return pricingHistory;
|
||||
}
|
||||
//#endregion
|
||||
@@ -102,12 +104,16 @@ export class ConfigurationsService {
|
||||
}
|
||||
|
||||
async getPrivacyPolicy() {
|
||||
const privacyPolicy = await this.privacyPolicyModel.findOne({ isActive: true });
|
||||
const privacyPolicy = await this.privacyPolicyModel.findOne({
|
||||
isActive: true,
|
||||
});
|
||||
return privacyPolicy;
|
||||
}
|
||||
|
||||
async getPrivacyPolicyHistory() {
|
||||
const policyHistory = await this.privacyPolicyModel.find().sort({ lastUpdated: -1 });
|
||||
const policyHistory = await this.privacyPolicyModel
|
||||
.find()
|
||||
.sort({ lastUpdated: -1 });
|
||||
return policyHistory;
|
||||
}
|
||||
//#endregion
|
||||
@@ -130,7 +136,9 @@ export class ConfigurationsService {
|
||||
}
|
||||
|
||||
async getSearchSlogan() {
|
||||
const searchSlogan = await this.searchSloganModel.findOne({ isActive: true });
|
||||
const searchSlogan = await this.searchSloganModel.findOne({
|
||||
isActive: true,
|
||||
});
|
||||
return searchSlogan;
|
||||
}
|
||||
//#endregion
|
||||
@@ -185,7 +193,7 @@ export class ConfigurationsService {
|
||||
if (!faq) return null;
|
||||
|
||||
const filteredQuestions = faq.questions.filter(
|
||||
(q) => q.category === category && q.isActive
|
||||
(q) => q.category === category && q.isActive,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -200,8 +208,8 @@ export class ConfigurationsService {
|
||||
throw new Error('FAQ document not found. Please create FAQ first.');
|
||||
}
|
||||
|
||||
const maxOrder = Math.max(...faq.questions.map(q => q.order), 0);
|
||||
|
||||
const maxOrder = Math.max(...faq.questions.map((q) => q.order), 0);
|
||||
|
||||
faq.questions.push({
|
||||
question,
|
||||
answer,
|
||||
@@ -215,13 +223,16 @@ export class ConfigurationsService {
|
||||
return faq;
|
||||
}
|
||||
|
||||
async updateFaqQuestion(questionIndex: number, updatedQuestion: Partial<{
|
||||
question: string;
|
||||
answer: string;
|
||||
category: string;
|
||||
order: number;
|
||||
isActive: boolean;
|
||||
}>) {
|
||||
async updateFaqQuestion(
|
||||
questionIndex: number,
|
||||
updatedQuestion: Partial<{
|
||||
question: string;
|
||||
answer: string;
|
||||
category: string;
|
||||
order: number;
|
||||
isActive: boolean;
|
||||
}>,
|
||||
) {
|
||||
const faq = await this.faqModel.findOne({ isActive: true });
|
||||
if (!faq || !faq.questions[questionIndex]) {
|
||||
throw new Error('FAQ or question not found');
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsBoolean, IsArray, IsNumber, ValidateNested } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class TeamMemberDto {
|
||||
@@ -26,7 +33,9 @@ export class AboutUsDto {
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: 'Main about us content in HTML or markdown format' })
|
||||
@ApiProperty({
|
||||
description: 'Main about us content in HTML or markdown format',
|
||||
})
|
||||
@IsString()
|
||||
content: string;
|
||||
|
||||
@@ -40,7 +49,11 @@ export class AboutUsDto {
|
||||
@IsString()
|
||||
vision?: string;
|
||||
|
||||
@ApiProperty({ description: 'Company values', type: [String], required: false })
|
||||
@ApiProperty({
|
||||
description: 'Company values',
|
||||
type: [String],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@@ -61,7 +74,11 @@ export class AboutUsDto {
|
||||
@IsString()
|
||||
companyDescription?: string;
|
||||
|
||||
@ApiProperty({ description: 'Team members', type: [TeamMemberDto], required: false })
|
||||
@ApiProperty({
|
||||
description: 'Team members',
|
||||
type: [TeamMemberDto],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@@ -73,8 +90,12 @@ export class AboutUsDto {
|
||||
@IsString()
|
||||
updatedBy?: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether content is active', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Whether content is active',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsString, IsOptional, IsBoolean, Min } from 'class-validator';
|
||||
import {
|
||||
IsNumber,
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
|
||||
export class AdPricingDto {
|
||||
@ApiProperty({ description: 'Price for line ads', example: 100, minimum: 0 })
|
||||
@@ -7,7 +13,11 @@ export class AdPricingDto {
|
||||
@Min(0)
|
||||
lineAdPrice: number;
|
||||
|
||||
@ApiProperty({ description: 'Price for poster ads', example: 200, minimum: 0 })
|
||||
@ApiProperty({
|
||||
description: 'Price for poster ads',
|
||||
example: 200,
|
||||
minimum: 0,
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
posterAdPrice: number;
|
||||
@@ -17,7 +27,11 @@ export class AdPricingDto {
|
||||
@Min(0)
|
||||
videoAdPrice: number;
|
||||
|
||||
@ApiProperty({ description: 'Currency code', example: 'INR', required: false })
|
||||
@ApiProperty({
|
||||
description: 'Currency code',
|
||||
example: 'INR',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
currency?: string;
|
||||
@@ -27,8 +41,12 @@ export class AdPricingDto {
|
||||
@IsString()
|
||||
updatedBy?: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether pricing is active', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Whether pricing is active',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsBoolean, IsArray, IsNumber, ValidateNested } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class CoordinatesDto {
|
||||
@@ -47,11 +54,17 @@ export class ContactPageDto {
|
||||
@IsString()
|
||||
companyName: string;
|
||||
|
||||
@ApiProperty({ description: 'Primary email address', example: 'contact@paisaads.com' })
|
||||
@ApiProperty({
|
||||
description: 'Primary email address',
|
||||
example: 'contact@paisaads.com',
|
||||
})
|
||||
@IsString()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: 'Primary phone number', example: '+91 9876543210' })
|
||||
@ApiProperty({
|
||||
description: 'Primary phone number',
|
||||
example: '+91 9876543210',
|
||||
})
|
||||
@IsString()
|
||||
phone: string;
|
||||
|
||||
@@ -84,19 +97,31 @@ export class ContactPageDto {
|
||||
@IsString()
|
||||
country?: string;
|
||||
|
||||
@ApiProperty({ description: 'GPS coordinates', type: CoordinatesDto, required: false })
|
||||
@ApiProperty({
|
||||
description: 'GPS coordinates',
|
||||
type: CoordinatesDto,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => CoordinatesDto)
|
||||
coordinates?: CoordinatesDto;
|
||||
|
||||
@ApiProperty({ description: 'Social media links', type: [String], required: false })
|
||||
@ApiProperty({
|
||||
description: 'Social media links',
|
||||
type: [String],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
socialMediaLinks?: string[];
|
||||
|
||||
@ApiProperty({ description: 'Business hours', type: BusinessHoursDto, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Business hours',
|
||||
type: BusinessHoursDto,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => BusinessHoursDto)
|
||||
@@ -122,13 +147,20 @@ export class ContactPageDto {
|
||||
@IsString()
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ description: 'User who updated the contact info', required: false })
|
||||
@ApiProperty({
|
||||
description: 'User who updated the contact info',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
updatedBy?: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether contact info is active', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Whether contact info is active',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsBoolean, IsArray, IsNumber, ValidateNested } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsArray,
|
||||
IsNumber,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class FaqQuestionDto {
|
||||
@@ -19,24 +26,31 @@ class FaqQuestionDto {
|
||||
@IsNumber()
|
||||
order: number;
|
||||
|
||||
@ApiProperty({ description: 'Whether question is active', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Whether question is active',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export class FaqDto {
|
||||
@ApiProperty({ description: 'FAQ questions and answers', type: [FaqQuestionDto] })
|
||||
@ApiProperty({
|
||||
description: 'FAQ questions and answers',
|
||||
type: [FaqQuestionDto],
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => FaqQuestionDto)
|
||||
questions: FaqQuestionDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'FAQ categories',
|
||||
type: [String],
|
||||
@ApiProperty({
|
||||
description: 'FAQ categories',
|
||||
type: [String],
|
||||
example: ['General', 'Account', 'Payments', 'Ads', 'Technical'],
|
||||
required: false
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@@ -48,7 +62,10 @@ export class FaqDto {
|
||||
@IsString()
|
||||
introduction?: string;
|
||||
|
||||
@ApiProperty({ description: 'Contact information for additional help', required: false })
|
||||
@ApiProperty({
|
||||
description: 'Contact information for additional help',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactInfo?: string;
|
||||
@@ -58,8 +75,12 @@ export class FaqDto {
|
||||
@IsString()
|
||||
updatedBy?: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether FAQ is active', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Whether FAQ is active',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsBoolean, IsDateString } from 'class-validator';
|
||||
|
||||
export class PrivacyPolicyDto {
|
||||
@ApiProperty({ description: 'Privacy policy content in HTML or markdown format' })
|
||||
@ApiProperty({
|
||||
description: 'Privacy policy content in HTML or markdown format',
|
||||
})
|
||||
@IsString()
|
||||
content: string;
|
||||
|
||||
@@ -20,8 +22,12 @@ export class PrivacyPolicyDto {
|
||||
@IsString()
|
||||
updatedBy?: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether policy is active', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Whether policy is active',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,36 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsBoolean } from 'class-validator';
|
||||
|
||||
export class SearchSloganDto {
|
||||
@ApiProperty({ description: 'Main slogan text', example: 'Find Your Perfect Deal Today!' })
|
||||
@ApiProperty({
|
||||
description: 'Main slogan text',
|
||||
example: 'Find Your Perfect Deal Today!',
|
||||
})
|
||||
@IsString()
|
||||
mainSlogan: string;
|
||||
|
||||
@ApiProperty({ description: 'Sub slogan text', required: false, example: 'Discover thousands of listings in your area' })
|
||||
@ApiProperty({
|
||||
description: 'Sub slogan text',
|
||||
required: false,
|
||||
example: 'Discover thousands of listings in your area',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
subSlogan?: string;
|
||||
|
||||
@ApiProperty({ description: 'Show slogan on search page', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Show slogan on search page',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
showOnSearchPage?: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Show slogan on home page', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Show slogan on home page',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
showOnHomePage?: boolean;
|
||||
@@ -26,8 +41,12 @@ export class SearchSloganDto {
|
||||
@IsString()
|
||||
updatedBy?: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether slogan is active', default: true, required: false })
|
||||
@ApiProperty({
|
||||
description: 'Whether slogan is active',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ export class AboutUs {
|
||||
@Prop()
|
||||
companyDescription: string;
|
||||
|
||||
@Prop({ type: [{ name: String, position: String, bio: String, image: String }] })
|
||||
@Prop({
|
||||
type: [{ name: String, position: String, bio: String, image: String }],
|
||||
})
|
||||
teamMembers: Array<{
|
||||
name: string;
|
||||
position: string;
|
||||
@@ -47,4 +49,4 @@ export class AboutUs {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const AboutUsSchema = SchemaFactory.createForClass(AboutUs);
|
||||
export const AboutUsSchema = SchemaFactory.createForClass(AboutUs);
|
||||
|
||||
@@ -27,4 +27,4 @@ export class AdPricing {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const AdPricingSchema = SchemaFactory.createForClass(AdPricing);
|
||||
export const AdPricingSchema = SchemaFactory.createForClass(AdPricing);
|
||||
|
||||
@@ -41,7 +41,17 @@ export class ContactPage {
|
||||
@Prop({ type: [String] })
|
||||
socialMediaLinks: string[];
|
||||
|
||||
@Prop({ type: { monday: String, tuesday: String, wednesday: String, thursday: String, friday: String, saturday: String, sunday: String } })
|
||||
@Prop({
|
||||
type: {
|
||||
monday: String,
|
||||
tuesday: String,
|
||||
wednesday: String,
|
||||
thursday: String,
|
||||
friday: String,
|
||||
saturday: String,
|
||||
sunday: String,
|
||||
},
|
||||
})
|
||||
businessHours: {
|
||||
monday: string;
|
||||
tuesday: string;
|
||||
@@ -74,4 +84,4 @@ export class ContactPage {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const ContactPageSchema = SchemaFactory.createForClass(ContactPage);
|
||||
export const ContactPageSchema = SchemaFactory.createForClass(ContactPage);
|
||||
|
||||
@@ -5,7 +5,18 @@ export type FaqDocument = Faq & Document;
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Faq {
|
||||
@Prop({ type: [{ question: String, answer: String, category: String, order: Number, isActive: { type: Boolean, default: true } }], required: true })
|
||||
@Prop({
|
||||
type: [
|
||||
{
|
||||
question: String,
|
||||
answer: String,
|
||||
category: String,
|
||||
order: Number,
|
||||
isActive: { type: Boolean, default: true },
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
})
|
||||
questions: Array<{
|
||||
question: string;
|
||||
answer: string;
|
||||
@@ -14,7 +25,10 @@ export class Faq {
|
||||
isActive: boolean;
|
||||
}>;
|
||||
|
||||
@Prop({ type: [String], default: ['General', 'Account', 'Payments', 'Ads', 'Technical'] })
|
||||
@Prop({
|
||||
type: [String],
|
||||
default: ['General', 'Account', 'Payments', 'Ads', 'Technical'],
|
||||
})
|
||||
categories: string[];
|
||||
|
||||
@Prop()
|
||||
@@ -33,4 +47,4 @@ export class Faq {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const FaqSchema = SchemaFactory.createForClass(Faq);
|
||||
export const FaqSchema = SchemaFactory.createForClass(Faq);
|
||||
|
||||
@@ -24,4 +24,4 @@ export class PrivacyPolicy {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const PrivacyPolicySchema = SchemaFactory.createForClass(PrivacyPolicy);
|
||||
export const PrivacyPolicySchema = SchemaFactory.createForClass(PrivacyPolicy);
|
||||
|
||||
@@ -27,4 +27,4 @@ export class SearchSlogan {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const SearchSloganSchema = SchemaFactory.createForClass(SearchSlogan);
|
||||
export const SearchSloganSchema = SchemaFactory.createForClass(SearchSlogan);
|
||||
|
||||
@@ -12,4 +12,5 @@ export class TermsAndConditions {
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const TermsAndConditionsSchema = SchemaFactory.createForClass(TermsAndConditions);
|
||||
export const TermsAndConditionsSchema =
|
||||
SchemaFactory.createForClass(TermsAndConditions);
|
||||
|
||||
@@ -32,7 +32,9 @@ export default class LineAdSeeder implements Seeder {
|
||||
// Check if line ads already exist
|
||||
const existingLineAds = await lineAdRepo.count();
|
||||
if (existingLineAds > 0) {
|
||||
console.log(`${existingLineAds} line ads already exist. Skipping line ad seeder.`);
|
||||
console.log(
|
||||
`${existingLineAds} line ads already exist. Skipping line ad seeder.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,7 +68,9 @@ export default class LineAdSeeder implements Seeder {
|
||||
|
||||
const createdCategories: MainCategory[] = [];
|
||||
for (const cat of categories) {
|
||||
let mainCategory = await mainCategoryRepo.findOne({ where: { name: cat.name } });
|
||||
let mainCategory = await mainCategoryRepo.findOne({
|
||||
where: { name: cat.name },
|
||||
});
|
||||
if (!mainCategory) {
|
||||
mainCategory = mainCategoryRepo.create({
|
||||
name: cat.name,
|
||||
@@ -81,27 +85,113 @@ export default class LineAdSeeder implements Seeder {
|
||||
|
||||
// Create subcategories
|
||||
const subcategories = {
|
||||
Electronics: ['Mobile Phones', 'Laptops', 'TV & Audio', 'Cameras', 'Gaming', 'Accessories'],
|
||||
Fashion: ['Men\'s Clothing', 'Women\'s Clothing', 'Accessories', 'Footwear', 'Watches', 'Bags'],
|
||||
'Real Estate': ['Residential', 'Commercial', 'Land', 'Rental', 'PG/Hostel', 'Office Space'],
|
||||
Automotive: ['Cars', 'Bikes', 'Auto Parts', 'Services', 'Commercial Vehicles', 'Accessories'],
|
||||
Entertainment: ['Movies', 'Music', 'Gaming', 'Events', 'Books', 'Tickets'],
|
||||
'Health & Beauty': ['Healthcare', 'Beauty Products', 'Fitness', 'Wellness', 'Medical Equipment', 'Supplements'],
|
||||
'Food & Beverages': ['Restaurants', 'Cafes', 'Groceries', 'Catering', 'Food Delivery', 'Beverages'],
|
||||
Services: ['Home Services', 'Professional Services', 'Repair Services', 'Consulting', 'Legal Services', 'Financial Services'],
|
||||
Jobs: ['IT Jobs', 'Sales Jobs', 'Marketing Jobs', 'Healthcare Jobs', 'Education Jobs', 'Part Time Jobs'],
|
||||
Education: ['Tuitions', 'Coaching Classes', 'Online Courses', 'Schools', 'Colleges', 'Skill Development'],
|
||||
'Home & Garden': ['Furniture', 'Home Decor', 'Appliances', 'Garden Tools', 'Lighting', 'Kitchen Items'],
|
||||
'Sports & Fitness': ['Gym Equipment', 'Sports Goods', 'Outdoor Sports', 'Fitness Classes', 'Sports Coaching', 'Yoga Classes'],
|
||||
Electronics: [
|
||||
'Mobile Phones',
|
||||
'Laptops',
|
||||
'TV & Audio',
|
||||
'Cameras',
|
||||
'Gaming',
|
||||
'Accessories',
|
||||
],
|
||||
Fashion: [
|
||||
"Men's Clothing",
|
||||
"Women's Clothing",
|
||||
'Accessories',
|
||||
'Footwear',
|
||||
'Watches',
|
||||
'Bags',
|
||||
],
|
||||
'Real Estate': [
|
||||
'Residential',
|
||||
'Commercial',
|
||||
'Land',
|
||||
'Rental',
|
||||
'PG/Hostel',
|
||||
'Office Space',
|
||||
],
|
||||
Automotive: [
|
||||
'Cars',
|
||||
'Bikes',
|
||||
'Auto Parts',
|
||||
'Services',
|
||||
'Commercial Vehicles',
|
||||
'Accessories',
|
||||
],
|
||||
Entertainment: [
|
||||
'Movies',
|
||||
'Music',
|
||||
'Gaming',
|
||||
'Events',
|
||||
'Books',
|
||||
'Tickets',
|
||||
],
|
||||
'Health & Beauty': [
|
||||
'Healthcare',
|
||||
'Beauty Products',
|
||||
'Fitness',
|
||||
'Wellness',
|
||||
'Medical Equipment',
|
||||
'Supplements',
|
||||
],
|
||||
'Food & Beverages': [
|
||||
'Restaurants',
|
||||
'Cafes',
|
||||
'Groceries',
|
||||
'Catering',
|
||||
'Food Delivery',
|
||||
'Beverages',
|
||||
],
|
||||
Services: [
|
||||
'Home Services',
|
||||
'Professional Services',
|
||||
'Repair Services',
|
||||
'Consulting',
|
||||
'Legal Services',
|
||||
'Financial Services',
|
||||
],
|
||||
Jobs: [
|
||||
'IT Jobs',
|
||||
'Sales Jobs',
|
||||
'Marketing Jobs',
|
||||
'Healthcare Jobs',
|
||||
'Education Jobs',
|
||||
'Part Time Jobs',
|
||||
],
|
||||
Education: [
|
||||
'Tuitions',
|
||||
'Coaching Classes',
|
||||
'Online Courses',
|
||||
'Schools',
|
||||
'Colleges',
|
||||
'Skill Development',
|
||||
],
|
||||
'Home & Garden': [
|
||||
'Furniture',
|
||||
'Home Decor',
|
||||
'Appliances',
|
||||
'Garden Tools',
|
||||
'Lighting',
|
||||
'Kitchen Items',
|
||||
],
|
||||
'Sports & Fitness': [
|
||||
'Gym Equipment',
|
||||
'Sports Goods',
|
||||
'Outdoor Sports',
|
||||
'Fitness Classes',
|
||||
'Sports Coaching',
|
||||
'Yoga Classes',
|
||||
],
|
||||
};
|
||||
|
||||
const categoryOneMap = new Map();
|
||||
for (const [mainCatName, subCats] of Object.entries(subcategories)) {
|
||||
const mainCategory = createdCategories.find(c => c.name === mainCatName);
|
||||
const mainCategory = createdCategories.find(
|
||||
(c) => c.name === mainCatName,
|
||||
);
|
||||
if (mainCategory) {
|
||||
for (const subCatName of subCats) {
|
||||
let categoryOne = await categoryOneRepo.findOne({
|
||||
where: { name: subCatName, parent: { id: mainCategory.id } }
|
||||
let categoryOne = await categoryOneRepo.findOne({
|
||||
where: { name: subCatName, parent: { id: mainCategory.id } },
|
||||
});
|
||||
if (!categoryOne) {
|
||||
categoryOne = categoryOneRepo.create({
|
||||
@@ -120,7 +210,7 @@ export default class LineAdSeeder implements Seeder {
|
||||
const imageFiles = [
|
||||
'1746748722095.png',
|
||||
'1747127356626.jpg',
|
||||
'1747127917692.png',
|
||||
'1747127917692.png',
|
||||
'1747127958198.jpg',
|
||||
'1747127984078.jpg',
|
||||
'1747128020849.png',
|
||||
@@ -165,7 +255,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Real Estate',
|
||||
categoryOne: 'Residential',
|
||||
content: '3BHK flat for sale in Bandra West. Prime location, sea view, fully furnished. Ready to move. 1200 sq ft. Parking available.',
|
||||
content:
|
||||
'3BHK flat for sale in Bandra West. Prime location, sea view, fully furnished. Ready to move. 1200 sq ft. Parking available.',
|
||||
contactOne: 9876543210,
|
||||
contactTwo: 8765432109,
|
||||
state: 'Maharashtra',
|
||||
@@ -185,7 +276,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Real Estate',
|
||||
categoryOne: 'Rental',
|
||||
content: '2BHK apartment for rent in Koramangala. Well ventilated, near metro station. Rs 25,000/month. Family preferred.',
|
||||
content:
|
||||
'2BHK apartment for rent in Koramangala. Well ventilated, near metro station. Rs 25,000/month. Family preferred.',
|
||||
contactOne: 9988776655,
|
||||
contactTwo: undefined,
|
||||
state: 'Karnataka',
|
||||
@@ -207,7 +299,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Jobs',
|
||||
categoryOne: 'IT Jobs',
|
||||
content: 'Software Developer required. React, Node.js, 2+ years exp. Salary: 8-12 LPA. Work from home available. Immediate joining.',
|
||||
content:
|
||||
'Software Developer required. React, Node.js, 2+ years exp. Salary: 8-12 LPA. Work from home available. Immediate joining.',
|
||||
contactOne: 7766554433,
|
||||
contactTwo: 9988112233,
|
||||
state: 'Delhi',
|
||||
@@ -227,7 +320,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Jobs',
|
||||
categoryOne: 'Sales Jobs',
|
||||
content: 'Sales Executive needed for leading FMCG company. Field sales, Delhi NCR. Good communication skills. Attractive incentives.',
|
||||
content:
|
||||
'Sales Executive needed for leading FMCG company. Field sales, Delhi NCR. Good communication skills. Attractive incentives.',
|
||||
contactOne: 8877665544,
|
||||
contactTwo: undefined,
|
||||
state: 'Haryana',
|
||||
@@ -249,7 +343,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Automotive',
|
||||
categoryOne: 'Cars',
|
||||
content: 'Maruti Swift 2019 for sale. Single owner, well maintained. 45,000 km driven. All documents ready. Price negotiable.',
|
||||
content:
|
||||
'Maruti Swift 2019 for sale. Single owner, well maintained. 45,000 km driven. All documents ready. Price negotiable.',
|
||||
contactOne: 9876543211,
|
||||
contactTwo: 8765432110,
|
||||
state: 'Tamil Nadu',
|
||||
@@ -269,7 +364,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Automotive',
|
||||
categoryOne: 'Bikes',
|
||||
content: 'Honda Activa 5G, 2020 model. Excellent condition, serviced regularly. Single owner. Price: Rs 65,000 negotiable.',
|
||||
content:
|
||||
'Honda Activa 5G, 2020 model. Excellent condition, serviced regularly. Single owner. Price: Rs 65,000 negotiable.',
|
||||
contactOne: 9876543212,
|
||||
contactTwo: undefined,
|
||||
state: 'West Bengal',
|
||||
@@ -291,7 +387,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Services',
|
||||
categoryOne: 'Home Services',
|
||||
content: 'Professional AC repair & servicing. All brands. 24/7 service. Experienced technicians. Warranty provided. Call now!',
|
||||
content:
|
||||
'Professional AC repair & servicing. All brands. 24/7 service. Experienced technicians. Warranty provided. Call now!',
|
||||
contactOne: 7788996655,
|
||||
contactTwo: 9988776644,
|
||||
state: 'Gujarat',
|
||||
@@ -311,7 +408,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Services',
|
||||
categoryOne: 'Professional Services',
|
||||
content: 'CA Services - Income Tax, GST, Company Registration, Audit. 15+ years experience. Reasonable rates. Free consultation.',
|
||||
content:
|
||||
'CA Services - Income Tax, GST, Company Registration, Audit. 15+ years experience. Reasonable rates. Free consultation.',
|
||||
contactOne: 9876543213,
|
||||
contactTwo: undefined,
|
||||
state: 'Rajasthan',
|
||||
@@ -333,7 +431,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Electronics',
|
||||
categoryOne: 'Mobile Phones',
|
||||
content: 'iPhone 13 128GB for sale. 11 months old, excellent condition. Bill & box available. No scratches. Price: Rs 55,000.',
|
||||
content:
|
||||
'iPhone 13 128GB for sale. 11 months old, excellent condition. Bill & box available. No scratches. Price: Rs 55,000.',
|
||||
contactOne: 8877665511,
|
||||
contactTwo: 7766554422,
|
||||
state: 'Punjab',
|
||||
@@ -353,7 +452,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Electronics',
|
||||
categoryOne: 'Laptops',
|
||||
content: 'Dell Inspiron 15 - Intel i5, 8GB RAM, 512GB SSD. Perfect for work & study. 2 years warranty remaining.',
|
||||
content:
|
||||
'Dell Inspiron 15 - Intel i5, 8GB RAM, 512GB SSD. Perfect for work & study. 2 years warranty remaining.',
|
||||
contactOne: 9988776633,
|
||||
contactTwo: undefined,
|
||||
state: 'Telangana',
|
||||
@@ -374,8 +474,9 @@ export default class LineAdSeeder implements Seeder {
|
||||
// Fashion - mix of with/without images
|
||||
{
|
||||
mainCategory: 'Fashion',
|
||||
categoryOne: 'Women\'s Clothing',
|
||||
content: 'Designer sarees collection. Wedding & party wear. Premium quality silk. Direct from manufacturer. Wholesale rates available.',
|
||||
categoryOne: "Women's Clothing",
|
||||
content:
|
||||
'Designer sarees collection. Wedding & party wear. Premium quality silk. Direct from manufacturer. Wholesale rates available.',
|
||||
contactOne: 7766554411,
|
||||
contactTwo: 8877665522,
|
||||
state: 'Uttar Pradesh',
|
||||
@@ -394,8 +495,9 @@ export default class LineAdSeeder implements Seeder {
|
||||
},
|
||||
{
|
||||
mainCategory: 'Fashion',
|
||||
categoryOne: 'Men\'s Clothing',
|
||||
content: 'Branded shirts & trousers. Formal & casual wear. All sizes available. Best quality at reasonable prices. Home delivery.',
|
||||
categoryOne: "Men's Clothing",
|
||||
content:
|
||||
'Branded shirts & trousers. Formal & casual wear. All sizes available. Best quality at reasonable prices. Home delivery.',
|
||||
contactOne: 9876543214,
|
||||
contactTwo: undefined,
|
||||
state: 'Kerala',
|
||||
@@ -417,7 +519,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Education',
|
||||
categoryOne: 'Tuitions',
|
||||
content: 'Mathematics & Physics tuition for 11th & 12th. IIT JEE preparation. 10+ years experience. Online & offline classes.',
|
||||
content:
|
||||
'Mathematics & Physics tuition for 11th & 12th. IIT JEE preparation. 10+ years experience. Online & offline classes.',
|
||||
contactOne: 8877665533,
|
||||
contactTwo: 7766554444,
|
||||
state: 'Madhya Pradesh',
|
||||
@@ -437,7 +540,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Education',
|
||||
categoryOne: 'Coaching Classes',
|
||||
content: 'UPSC Civil Services coaching. Experienced faculty, study material provided. Demo class available. Limited seats.',
|
||||
content:
|
||||
'UPSC Civil Services coaching. Experienced faculty, study material provided. Demo class available. Limited seats.',
|
||||
contactOne: 9876543215,
|
||||
contactTwo: undefined,
|
||||
state: 'Delhi',
|
||||
@@ -459,7 +563,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Health & Beauty',
|
||||
categoryOne: 'Beauty Products',
|
||||
content: 'Skincare products - Natural & organic. Anti-aging creams, face wash, moisturizers. All skin types. Chemical-free.',
|
||||
content:
|
||||
'Skincare products - Natural & organic. Anti-aging creams, face wash, moisturizers. All skin types. Chemical-free.',
|
||||
contactOne: 7788996644,
|
||||
contactTwo: 9988776655,
|
||||
state: 'Goa',
|
||||
@@ -479,7 +584,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Health & Beauty',
|
||||
categoryOne: 'Fitness',
|
||||
content: 'Personal trainer available. Weight loss, muscle building, yoga. Home visits. Certified trainer with 5+ years experience.',
|
||||
content:
|
||||
'Personal trainer available. Weight loss, muscle building, yoga. Home visits. Certified trainer with 5+ years experience.',
|
||||
contactOne: 8877665544,
|
||||
contactTwo: undefined,
|
||||
state: 'Odisha',
|
||||
@@ -501,7 +607,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Home & Garden',
|
||||
categoryOne: 'Furniture',
|
||||
content: 'Wooden dining table set for sale. 6 seater, excellent condition. Teak wood, carved design. Must sell urgently.',
|
||||
content:
|
||||
'Wooden dining table set for sale. 6 seater, excellent condition. Teak wood, carved design. Must sell urgently.',
|
||||
contactOne: 9876543216,
|
||||
contactTwo: 8765432111,
|
||||
state: 'Assam',
|
||||
@@ -521,7 +628,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Home & Garden',
|
||||
categoryOne: 'Appliances',
|
||||
content: 'Samsung refrigerator 265L, double door. 3 years old, working perfectly. Moving out sale. Price negotiable.',
|
||||
content:
|
||||
'Samsung refrigerator 265L, double door. 3 years old, working perfectly. Moving out sale. Price negotiable.',
|
||||
contactOne: 7766554433,
|
||||
contactTwo: undefined,
|
||||
state: 'Jharkhand',
|
||||
@@ -543,7 +651,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Sports & Fitness',
|
||||
categoryOne: 'Gym Equipment',
|
||||
content: 'Home gym setup for sale. Dumbbells, bench, barbell set. Excellent condition. Complete package deal available.',
|
||||
content:
|
||||
'Home gym setup for sale. Dumbbells, bench, barbell set. Excellent condition. Complete package deal available.',
|
||||
contactOne: 8877665555,
|
||||
contactTwo: 7766554466,
|
||||
state: 'Bihar',
|
||||
@@ -563,7 +672,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Sports & Fitness',
|
||||
categoryOne: 'Sports Coaching',
|
||||
content: 'Cricket coaching for kids & adults. Professional coach, group & individual sessions. Ground available. Weekend batches.',
|
||||
content:
|
||||
'Cricket coaching for kids & adults. Professional coach, group & individual sessions. Ground available. Weekend batches.',
|
||||
contactOne: 9876543217,
|
||||
contactTwo: undefined,
|
||||
state: 'Uttarakhand',
|
||||
@@ -585,7 +695,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Food & Beverages',
|
||||
categoryOne: 'Restaurants',
|
||||
content: 'New restaurant opening! Best North Indian cuisine. Family restaurant, home delivery available. Grand opening discounts.',
|
||||
content:
|
||||
'New restaurant opening! Best North Indian cuisine. Family restaurant, home delivery available. Grand opening discounts.',
|
||||
contactOne: 7788996666,
|
||||
contactTwo: 8877665577,
|
||||
state: 'Himachal Pradesh',
|
||||
@@ -605,7 +716,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Food & Beverages',
|
||||
categoryOne: 'Catering',
|
||||
content: 'Wedding catering services. Multi-cuisine menu, experienced chefs. Packages for 50-500 guests. Free tasting session.',
|
||||
content:
|
||||
'Wedding catering services. Multi-cuisine menu, experienced chefs. Packages for 50-500 guests. Free tasting session.',
|
||||
contactOne: 9876543218,
|
||||
contactTwo: undefined,
|
||||
state: 'Chhattisgarh',
|
||||
@@ -627,7 +739,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Entertainment',
|
||||
categoryOne: 'Events',
|
||||
content: 'DJ services for parties & weddings. Latest sound system, lighting available. Experienced DJ, all types of music.',
|
||||
content:
|
||||
'DJ services for parties & weddings. Latest sound system, lighting available. Experienced DJ, all types of music.',
|
||||
contactOne: 8877665588,
|
||||
contactTwo: 7766554499,
|
||||
state: 'Tripura',
|
||||
@@ -647,7 +760,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Entertainment',
|
||||
categoryOne: 'Music',
|
||||
content: 'Guitar classes - Acoustic & electric. Beginner to advanced levels. Music theory included. Group & individual lessons.',
|
||||
content:
|
||||
'Guitar classes - Acoustic & electric. Beginner to advanced levels. Music theory included. Group & individual lessons.',
|
||||
contactOne: 9876543219,
|
||||
contactTwo: undefined,
|
||||
state: 'Nagaland',
|
||||
@@ -669,7 +783,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Services',
|
||||
categoryOne: 'Legal Services',
|
||||
content: 'Advocate services - Civil, criminal, family matters. High court practice. Consultation available. Reasonable fees.',
|
||||
content:
|
||||
'Advocate services - Civil, criminal, family matters. High court practice. Consultation available. Reasonable fees.',
|
||||
contactOne: 7788996677,
|
||||
contactTwo: undefined,
|
||||
state: 'Manipur',
|
||||
@@ -689,7 +804,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Electronics',
|
||||
categoryOne: 'Gaming',
|
||||
content: 'PS5 console for sale with 5 games. 1 year old, warranty remaining. Extra controller included. Genuine buyer contact.',
|
||||
content:
|
||||
'PS5 console for sale with 5 games. 1 year old, warranty remaining. Extra controller included. Genuine buyer contact.',
|
||||
contactOne: 8877665599,
|
||||
contactTwo: 9988776688,
|
||||
state: 'Arunachal Pradesh',
|
||||
@@ -709,7 +825,8 @@ export default class LineAdSeeder implements Seeder {
|
||||
{
|
||||
mainCategory: 'Jobs',
|
||||
categoryOne: 'Part Time Jobs',
|
||||
content: 'Data entry work from home. Flexible timing, weekly payment. Computer & internet required. Students welcome.',
|
||||
content:
|
||||
'Data entry work from home. Flexible timing, weekly payment. Computer & internet required. Students welcome.',
|
||||
contactOne: 9876543220,
|
||||
contactTwo: undefined,
|
||||
state: 'Mizoram',
|
||||
@@ -730,8 +847,12 @@ export default class LineAdSeeder implements Seeder {
|
||||
|
||||
// Create line ads
|
||||
for (const adData of lineAdsData) {
|
||||
const mainCategory = createdCategories.find(c => c.name === adData.mainCategory);
|
||||
const categoryOne = categoryOneMap.get(`${adData.mainCategory}-${adData.categoryOne}`);
|
||||
const mainCategory = createdCategories.find(
|
||||
(c) => c.name === adData.mainCategory,
|
||||
);
|
||||
const categoryOne = categoryOneMap.get(
|
||||
`${adData.mainCategory}-${adData.categoryOne}`,
|
||||
);
|
||||
|
||||
// Create images if the ad has them
|
||||
const adImages: Image[] = [];
|
||||
@@ -804,10 +925,13 @@ export default class LineAdSeeder implements Seeder {
|
||||
|
||||
await lineAdRepo.save(lineAd);
|
||||
const imageCount = adImages.length;
|
||||
const imageText = imageCount > 0 ? `with ${imageCount} image(s)` : 'without images';
|
||||
console.log(`Created line ad for ${adData.postedBy} in ${adData.city} (${imageText})`);
|
||||
const imageText =
|
||||
imageCount > 0 ? `with ${imageCount} image(s)` : 'without images';
|
||||
console.log(
|
||||
`Created line ad for ${adData.postedBy} in ${adData.city} (${imageText})`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Line Ad seeder completed successfully!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export default class PosterAdSeeder implements Seeder {
|
||||
// Check if poster ads already exist
|
||||
const existingPosterAds = await posterAdRepo.count();
|
||||
if (existingPosterAds > 0) {
|
||||
console.log(`${existingPosterAds} poster ads already exist. Skipping poster ad seeder.`);
|
||||
console.log(
|
||||
`${existingPosterAds} poster ads already exist. Skipping poster ad seeder.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,7 +63,9 @@ export default class PosterAdSeeder implements Seeder {
|
||||
|
||||
const createdCategories: MainCategory[] = [];
|
||||
for (const cat of categories) {
|
||||
let mainCategory = await mainCategoryRepo.findOne({ where: { name: cat.name } });
|
||||
let mainCategory = await mainCategoryRepo.findOne({
|
||||
where: { name: cat.name },
|
||||
});
|
||||
if (!mainCategory) {
|
||||
mainCategory = mainCategoryRepo.create({
|
||||
name: cat.name,
|
||||
@@ -77,21 +81,33 @@ export default class PosterAdSeeder implements Seeder {
|
||||
// Create subcategories
|
||||
const subcategories = {
|
||||
Electronics: ['Mobile Phones', 'Laptops', 'TV & Audio', 'Cameras'],
|
||||
Fashion: ['Men\'s Clothing', 'Women\'s Clothing', 'Accessories', 'Footwear'],
|
||||
Fashion: [
|
||||
"Men's Clothing",
|
||||
"Women's Clothing",
|
||||
'Accessories',
|
||||
'Footwear',
|
||||
],
|
||||
'Real Estate': ['Residential', 'Commercial', 'Land', 'Rental'],
|
||||
Automotive: ['Cars', 'Bikes', 'Auto Parts', 'Services'],
|
||||
Entertainment: ['Movies', 'Music', 'Gaming', 'Events'],
|
||||
'Health & Beauty': ['Healthcare', 'Beauty Products', 'Fitness', 'Wellness'],
|
||||
'Health & Beauty': [
|
||||
'Healthcare',
|
||||
'Beauty Products',
|
||||
'Fitness',
|
||||
'Wellness',
|
||||
],
|
||||
'Food & Beverages': ['Restaurants', 'Cafes', 'Groceries', 'Catering'],
|
||||
};
|
||||
|
||||
const categoryOneMap = new Map();
|
||||
for (const [mainCatName, subCats] of Object.entries(subcategories)) {
|
||||
const mainCategory = createdCategories.find(c => c.name === mainCatName);
|
||||
const mainCategory = createdCategories.find(
|
||||
(c) => c.name === mainCatName,
|
||||
);
|
||||
if (mainCategory) {
|
||||
for (const subCatName of subCats) {
|
||||
let categoryOne = await categoryOneRepo.findOne({
|
||||
where: { name: subCatName, parent: { id: mainCategory.id } }
|
||||
let categoryOne = await categoryOneRepo.findOne({
|
||||
where: { name: subCatName, parent: { id: mainCategory.id } },
|
||||
});
|
||||
if (!categoryOne) {
|
||||
categoryOne = categoryOneRepo.create({
|
||||
@@ -110,7 +126,7 @@ export default class PosterAdSeeder implements Seeder {
|
||||
const imageFiles = [
|
||||
'1746748722095.png',
|
||||
'1747127356626.jpg',
|
||||
'1747127917692.png',
|
||||
'1747127917692.png',
|
||||
'1747127958198.jpg',
|
||||
'1747127984078.jpg',
|
||||
'1747128020849.png',
|
||||
@@ -172,7 +188,7 @@ export default class PosterAdSeeder implements Seeder {
|
||||
},
|
||||
{
|
||||
mainCategory: 'Fashion',
|
||||
categoryOne: 'Women\'s Clothing',
|
||||
categoryOne: "Women's Clothing",
|
||||
imageFile: imageFiles[1],
|
||||
proofImage: proofImages[1],
|
||||
state: 'Karnataka',
|
||||
@@ -398,8 +414,12 @@ export default class PosterAdSeeder implements Seeder {
|
||||
|
||||
// Create poster ads
|
||||
for (const adData of posterAdsData) {
|
||||
const mainCategory = createdCategories.find(c => c.name === adData.mainCategory);
|
||||
const categoryOne = categoryOneMap.get(`${adData.mainCategory}-${adData.categoryOne}`);
|
||||
const mainCategory = createdCategories.find(
|
||||
(c) => c.name === adData.mainCategory,
|
||||
);
|
||||
const categoryOne = categoryOneMap.get(
|
||||
`${adData.mainCategory}-${adData.categoryOne}`,
|
||||
);
|
||||
|
||||
// Create poster image
|
||||
const posterImage = imageRepo.create({
|
||||
@@ -464,9 +484,11 @@ export default class PosterAdSeeder implements Seeder {
|
||||
});
|
||||
|
||||
await posterAdRepo.save(posterAd);
|
||||
console.log(`Created poster ad for ${adData.postedBy} in ${adData.city} (${adData.side}-${adData.position})`);
|
||||
console.log(
|
||||
`Created poster ad for ${adData.postedBy} in ${adData.city} (${adData.side}-${adData.position})`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Poster Ad seeder completed successfully!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export default class VideoAdSeeder implements Seeder {
|
||||
// Check if video ads already exist
|
||||
const existingVideoAds = await videoAdRepo.count();
|
||||
if (existingVideoAds > 0) {
|
||||
console.log(`${existingVideoAds} video ads already exist. Skipping video ad seeder.`);
|
||||
console.log(
|
||||
`${existingVideoAds} video ads already exist. Skipping video ad seeder.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,13 +43,12 @@ export default class VideoAdSeeder implements Seeder {
|
||||
relations: ['user'],
|
||||
take: 1,
|
||||
});
|
||||
|
||||
|
||||
if (!customers || customers.length === 0) {
|
||||
throw new Error('No customer found. Please run user seeder first.');
|
||||
}
|
||||
|
||||
const customer = customers[0];
|
||||
|
||||
const customer = customers[0];
|
||||
|
||||
// Create main categories for video ads
|
||||
const categories = [
|
||||
|
||||
@@ -66,6 +66,14 @@ export class CreateLineAdDto {
|
||||
@IsOptional()
|
||||
contactTwo?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
backgroundColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
textColor?: string;
|
||||
|
||||
@IsArray()
|
||||
dates: string[]; // e.g., ['2024-05-03','2024-05-04']
|
||||
|
||||
|
||||
@@ -24,27 +24,27 @@ import { AdPosition } from 'src/ad-position/entities/ad-position.entity';
|
||||
export class LineAd extends BaseEntity {
|
||||
@Generated('increment')
|
||||
@Column()
|
||||
sequenceNumber: number;
|
||||
sequenceNumber: number;
|
||||
|
||||
@Column({ nullable: true }) // Order ID can be derived from sequenceNumber + date etc in future
|
||||
orderId: number;
|
||||
orderId: number;
|
||||
|
||||
// --- Category Relations ---
|
||||
@ManyToOne(() => MainCategory, { eager: true, nullable: false })
|
||||
mainCategory: MainCategory;
|
||||
mainCategory: MainCategory;
|
||||
|
||||
@ManyToOne(() => CategoryOne, { eager: true, nullable: true })
|
||||
categoryOne?: CategoryOne;
|
||||
categoryOne?: CategoryOne;
|
||||
|
||||
@ManyToOne(() => CategoryTwo, { eager: true, nullable: true })
|
||||
categoryTwo?: CategoryTwo;
|
||||
categoryTwo?: CategoryTwo;
|
||||
|
||||
@ManyToOne(() => CategoryThree, { eager: true, nullable: true })
|
||||
categoryThree?: CategoryThree;
|
||||
categoryThree?: CategoryThree;
|
||||
|
||||
// --- Ad Content ---
|
||||
@Column()
|
||||
content: string;
|
||||
content: string;
|
||||
|
||||
@OneToMany(() => Image, (image) => image.lineAd, {
|
||||
cascade: true,
|
||||
@@ -52,31 +52,37 @@ export class LineAd extends BaseEntity {
|
||||
nullable: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
images: Image[];
|
||||
images: Image[];
|
||||
|
||||
@Column()
|
||||
state: string;
|
||||
state: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
sid: number;
|
||||
sid: number;
|
||||
|
||||
@Column()
|
||||
city: string;
|
||||
city: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
cid: number;
|
||||
cid: number;
|
||||
|
||||
@Column('simple-array')
|
||||
dates: string[];
|
||||
dates: string[];
|
||||
|
||||
@Column()
|
||||
postedBy: string;
|
||||
postedBy: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
contactOne: number;
|
||||
contactOne: number;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint' })
|
||||
contactTwo: number;
|
||||
contactTwo: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
backgroundColor: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
textColor: string;
|
||||
|
||||
// --- Payment ---
|
||||
@OneToOne(() => Payment, (payment) => payment.lineAd, {
|
||||
@@ -84,7 +90,7 @@ export class LineAd extends BaseEntity {
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn()
|
||||
payment: Payment;
|
||||
payment: Payment;
|
||||
|
||||
// --- Ad Workflow Status & Metadata ---
|
||||
@Column({
|
||||
@@ -92,22 +98,22 @@ export class LineAd extends BaseEntity {
|
||||
enum: AdStatus,
|
||||
default: AdStatus.DRAFT,
|
||||
})
|
||||
status: AdStatus;
|
||||
status: AdStatus;
|
||||
|
||||
@OneToMany(() => AdComment, (comment) => comment.lineAd, {})
|
||||
comments: AdComment[];
|
||||
comments: AdComment[];
|
||||
|
||||
// --- User/Admin Info ---
|
||||
|
||||
@ManyToOne(() => Customer, (customer) => customer.lineAds, {
|
||||
nullable: false,
|
||||
})
|
||||
@JoinColumn() // optional, helps clarity
|
||||
@JoinColumn() // optional, helps clarity
|
||||
customer: Customer;
|
||||
|
||||
// Soft delete
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
@OneToOne(() => AdPosition, (position) => position.lineAd)
|
||||
@JoinColumn()
|
||||
|
||||
@@ -120,15 +120,18 @@ export class LineAdService {
|
||||
cityId?: number;
|
||||
stateId?: number;
|
||||
}) {
|
||||
// Get today's date in local timezone
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
const todayISOString = today.toISOString().split('T')[0]; // 'YYYY-MM-DD'
|
||||
const todayISOString = today.getFullYear() + '-' +
|
||||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(today.getDate()).padStart(2, '0');
|
||||
|
||||
const where: FindOptionsWhere<LineAd> = {
|
||||
status: AdStatus.PUBLISHED,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
if (categoryId) {
|
||||
console.log(categoryId);
|
||||
where.mainCategory = { id: categoryId };
|
||||
}
|
||||
if (cityId) {
|
||||
@@ -137,37 +140,59 @@ export class LineAdService {
|
||||
if (stateId) {
|
||||
where.sid = stateId;
|
||||
}
|
||||
|
||||
// Fetch all published and active ads
|
||||
const lineAds = await this.lineAdRepo.find({
|
||||
where,
|
||||
relations: ['customer', 'customer.user', 'images', 'position'],
|
||||
relations: ['customer', 'customer.user', 'images', 'position', 'mainCategory', 'categoryOne', 'categoryTwo', 'categoryThree'],
|
||||
});
|
||||
|
||||
// Filter in JS for today's date in the dates array
|
||||
const filteredAds = lineAds.filter(
|
||||
(ad) =>
|
||||
Array.isArray(ad.dates) &&
|
||||
ad.dates.some((dateStr) => {
|
||||
const dateOnly = dateStr.split('T')[0];
|
||||
return dateOnly === todayISOString;
|
||||
}),
|
||||
);
|
||||
// Filter ads that should be displayed today
|
||||
const filteredAds = lineAds.filter((ad) => {
|
||||
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ad.dates.some((dateStr) => {
|
||||
try {
|
||||
// Handle both ISO string and date-only formats
|
||||
const adDate = new Date(dateStr);
|
||||
const adDateString = adDate.getFullYear() + '-' +
|
||||
String(adDate.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(adDate.getDate()).padStart(2, '0');
|
||||
|
||||
return adDateString === todayISOString;
|
||||
} catch (error) {
|
||||
console.error('Error parsing date:', dateStr, error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Return cleaned up ad data
|
||||
return filteredAds.map((ad) => {
|
||||
const {
|
||||
payment,
|
||||
customer,
|
||||
cid,
|
||||
sid,
|
||||
sequenceNumber,
|
||||
orderId,
|
||||
status,
|
||||
comments,
|
||||
dates,
|
||||
isActive,
|
||||
cid,
|
||||
sid,
|
||||
...serializedAd
|
||||
} = ad;
|
||||
return serializedAd;
|
||||
|
||||
return {
|
||||
...serializedAd,
|
||||
// Include essential customer info without sensitive data
|
||||
customerName: customer?.user?.name || null,
|
||||
// Include location for display
|
||||
cityId: cid,
|
||||
stateId: sid,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ async function bootstrap() {
|
||||
});
|
||||
} else {
|
||||
app.enableCors({
|
||||
origin: ['https://paisaads.in','http://paisaads.in/'],
|
||||
origin: ['https://paisaads.in', 'http://paisaads.in/'],
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||
credentials: true,
|
||||
allowedHeaders: 'Content-Type,Authorization',
|
||||
|
||||
@@ -170,15 +170,18 @@ export class PosterAdService {
|
||||
cityId?: number;
|
||||
stateId?: number;
|
||||
}) {
|
||||
// Get today's date in local timezone
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
const todayISOString = today.toISOString().split('T')[0]; // 'YYYY-MM-DD'
|
||||
const todayISOString = today.getFullYear() + '-' +
|
||||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(today.getDate()).padStart(2, '0');
|
||||
|
||||
const where: FindOptionsWhere<PosterAd> = {
|
||||
status: AdStatus.PUBLISHED,
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
if (categoryId) {
|
||||
console.log(categoryId);
|
||||
where.mainCategory = { id: categoryId };
|
||||
}
|
||||
if (cityId) {
|
||||
@@ -187,38 +190,59 @@ export class PosterAdService {
|
||||
if (stateId) {
|
||||
where.sid = stateId;
|
||||
}
|
||||
// Fetch all published and active ads
|
||||
const lineAds = await this.posterAdRepo.find({
|
||||
|
||||
// Fetch all published and active ads with all necessary relations
|
||||
const posterAds = await this.posterAdRepo.find({
|
||||
where,
|
||||
relations: ['position'],
|
||||
relations: ['customer', 'customer.user', 'image', 'position', 'mainCategory', 'categoryOne', 'categoryTwo', 'categoryThree'],
|
||||
});
|
||||
|
||||
// Filter in JS for today's date in the dates array
|
||||
const filteredAds = lineAds.filter(
|
||||
(ad) =>
|
||||
Array.isArray(ad.dates) &&
|
||||
ad.dates.some((dateStr) => {
|
||||
const dateOnly = dateStr.split('T')[0];
|
||||
return dateOnly === todayISOString;
|
||||
}),
|
||||
// ad,
|
||||
);
|
||||
// Filter ads that should be displayed today
|
||||
const filteredAds = posterAds.filter((ad) => {
|
||||
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ad.dates.some((dateStr) => {
|
||||
try {
|
||||
// Handle both ISO string and date-only formats
|
||||
const adDate = new Date(dateStr);
|
||||
const adDateString = adDate.getFullYear() + '-' +
|
||||
String(adDate.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(adDate.getDate()).padStart(2, '0');
|
||||
|
||||
return adDateString === todayISOString;
|
||||
} catch (error) {
|
||||
console.error('Error parsing date:', dateStr, error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Return cleaned up ad data
|
||||
return filteredAds.map((ad) => {
|
||||
const {
|
||||
payment,
|
||||
customer,
|
||||
cid,
|
||||
sid,
|
||||
sequenceNumber,
|
||||
orderId,
|
||||
status,
|
||||
comments,
|
||||
dates,
|
||||
isActive,
|
||||
cid,
|
||||
sid,
|
||||
...serializedAd
|
||||
} = ad;
|
||||
return serializedAd;
|
||||
|
||||
return {
|
||||
...serializedAd,
|
||||
// Include essential customer info without sensitive data
|
||||
customerName: customer?.user?.name || null,
|
||||
// Include location for display
|
||||
cityId: cid,
|
||||
stateId: sid,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,7 +283,7 @@ export class PosterAdService {
|
||||
// For CENTER_TOP and CENTER_BOTTOM sides, set position to 0 (will be treated as null)
|
||||
const newPosition =
|
||||
typeof position === 'object' && position !== null
|
||||
? (position as any).value
|
||||
? position.value
|
||||
: position;
|
||||
const positionValue =
|
||||
side === PositionType.CENTER_TOP || side === PositionType.CENTER_BOTTOM
|
||||
@@ -504,7 +528,9 @@ export class PosterAdService {
|
||||
if (filters?.userType) {
|
||||
query.andWhere('ad.postedBy = :userType', { userType: filters.userType });
|
||||
} else if (filters?.userTypes && filters.userTypes.length > 0) {
|
||||
query.andWhere('ad.postedBy IN (:...userTypes)', { userTypes: filters.userTypes });
|
||||
query.andWhere('ad.postedBy IN (:...userTypes)', {
|
||||
userTypes: filters.userTypes,
|
||||
});
|
||||
}
|
||||
|
||||
// Location filters
|
||||
@@ -523,16 +549,24 @@ export class PosterAdService {
|
||||
|
||||
// Category filters
|
||||
if (filters?.mainCategoryId) {
|
||||
query.andWhere('mainCategory.id = :mainCategoryId', { mainCategoryId: filters.mainCategoryId });
|
||||
query.andWhere('mainCategory.id = :mainCategoryId', {
|
||||
mainCategoryId: filters.mainCategoryId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryOneId) {
|
||||
query.andWhere('categoryOne.id = :categoryOneId', { categoryOneId: filters.categoryOneId });
|
||||
query.andWhere('categoryOne.id = :categoryOneId', {
|
||||
categoryOneId: filters.categoryOneId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryTwoId) {
|
||||
query.andWhere('categoryTwo.id = :categoryTwoId', { categoryTwoId: filters.categoryTwoId });
|
||||
query.andWhere('categoryTwo.id = :categoryTwoId', {
|
||||
categoryTwoId: filters.categoryTwoId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryThreeId) {
|
||||
query.andWhere('categoryThree.id = :categoryThreeId', { categoryThreeId: filters.categoryThreeId });
|
||||
query.andWhere('categoryThree.id = :categoryThreeId', {
|
||||
categoryThreeId: filters.categoryThreeId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryId) {
|
||||
query.andWhere(
|
||||
@@ -543,7 +577,9 @@ export class PosterAdService {
|
||||
|
||||
// Customer filter
|
||||
if (filters?.customerId) {
|
||||
query.andWhere('customer.id = :customerId', { customerId: filters.customerId });
|
||||
query.andWhere('customer.id = :customerId', {
|
||||
customerId: filters.customerId,
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { IsOptional, IsEnum, IsString, IsDateString, IsNumber, IsArray } from 'class-validator';
|
||||
import {
|
||||
IsOptional,
|
||||
IsEnum,
|
||||
IsString,
|
||||
IsDateString,
|
||||
IsNumber,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { AdStatus } from '../../common/enums/ad-status.enum';
|
||||
@@ -49,38 +56,47 @@ export class AdvancedFilterDto {
|
||||
originalValue: value,
|
||||
valueType: typeof value,
|
||||
valueIsArray: Array.isArray(value),
|
||||
valueConstructor: value?.constructor?.name
|
||||
valueConstructor: value?.constructor?.name,
|
||||
});
|
||||
|
||||
|
||||
// Handle string values (comma-separated)
|
||||
if (typeof value === 'string') {
|
||||
const result = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
|
||||
const result = value
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v.length > 0);
|
||||
console.log('DEBUG DTO Transform - adTypes result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Handle array values
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// Handle null/undefined
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
// Handle unexpected types - convert to array or return undefined
|
||||
console.warn('DEBUG DTO Transform - adTypes unexpected type, converting:', value);
|
||||
console.warn(
|
||||
'DEBUG DTO Transform - adTypes unexpected type, converting:',
|
||||
value,
|
||||
);
|
||||
if (typeof value === 'object' && value.length !== undefined) {
|
||||
// Try to convert array-like objects to actual arrays
|
||||
try {
|
||||
return Array.from(value);
|
||||
} catch (error) {
|
||||
console.error('DEBUG DTO Transform - adTypes failed to convert array-like object:', error);
|
||||
console.error(
|
||||
'DEBUG DTO Transform - adTypes failed to convert array-like object:',
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Single value - wrap in array
|
||||
return [value];
|
||||
})
|
||||
@@ -109,38 +125,47 @@ export class AdvancedFilterDto {
|
||||
originalValue: value,
|
||||
valueType: typeof value,
|
||||
valueIsArray: Array.isArray(value),
|
||||
valueConstructor: value?.constructor?.name
|
||||
valueConstructor: value?.constructor?.name,
|
||||
});
|
||||
|
||||
|
||||
// Handle string values (comma-separated)
|
||||
if (typeof value === 'string') {
|
||||
const result = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
|
||||
const result = value
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v.length > 0);
|
||||
console.log('DEBUG DTO Transform - userTypes result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Handle array values
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// Handle null/undefined
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
// Handle unexpected types - convert to array or return undefined
|
||||
console.warn('DEBUG DTO Transform - userTypes unexpected type, converting:', value);
|
||||
console.warn(
|
||||
'DEBUG DTO Transform - userTypes unexpected type, converting:',
|
||||
value,
|
||||
);
|
||||
if (typeof value === 'object' && value.length !== undefined) {
|
||||
// Try to convert array-like objects to actual arrays
|
||||
try {
|
||||
return Array.from(value);
|
||||
} catch (error) {
|
||||
console.error('DEBUG DTO Transform - userTypes failed to convert array-like object:', error);
|
||||
console.error(
|
||||
'DEBUG DTO Transform - userTypes failed to convert array-like object:',
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Single value - wrap in array
|
||||
return [value];
|
||||
})
|
||||
@@ -172,38 +197,47 @@ export class AdvancedFilterDto {
|
||||
originalValue: value,
|
||||
valueType: typeof value,
|
||||
valueIsArray: Array.isArray(value),
|
||||
valueConstructor: value?.constructor?.name
|
||||
valueConstructor: value?.constructor?.name,
|
||||
});
|
||||
|
||||
|
||||
// Handle string values (comma-separated)
|
||||
if (typeof value === 'string') {
|
||||
const result = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
|
||||
const result = value
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v.length > 0);
|
||||
console.log('DEBUG DTO Transform - statuses result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// Handle array values
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// Handle null/undefined
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
// Handle unexpected types - convert to array or return undefined
|
||||
console.warn('DEBUG DTO Transform - statuses unexpected type, converting:', value);
|
||||
console.warn(
|
||||
'DEBUG DTO Transform - statuses unexpected type, converting:',
|
||||
value,
|
||||
);
|
||||
if (typeof value === 'object' && value.length !== undefined) {
|
||||
// Try to convert array-like objects to actual arrays
|
||||
try {
|
||||
return Array.from(value);
|
||||
} catch (error) {
|
||||
console.error('DEBUG DTO Transform - statuses failed to convert array-like object:', error);
|
||||
console.error(
|
||||
'DEBUG DTO Transform - statuses failed to convert array-like object:',
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Single value - wrap in array
|
||||
return [value];
|
||||
})
|
||||
@@ -284,7 +318,8 @@ export class AdvancedFilterDto {
|
||||
categoryThreeId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Filter by any category ID (searches across all category levels)',
|
||||
description:
|
||||
'Filter by any category ID (searches across all category levels)',
|
||||
example: 'uuid-any-category',
|
||||
required: false,
|
||||
})
|
||||
@@ -400,4 +435,4 @@ export class AdvancedFilterResponse<T> {
|
||||
this.hasNext = page < this.totalPages;
|
||||
this.hasPrevious = page > 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ import { AdStatus } from 'src/common/enums/ad-status.enum';
|
||||
import { Roles } from 'src/auth/decorator/roles.decorator';
|
||||
import { Role } from 'src/common/enums/role.enum';
|
||||
import { AdType } from 'src/common/enums/ad-type';
|
||||
import { AdvancedFilterDto, AdvancedFilterResponse } from './dto/advanced-filter.dto';
|
||||
import {
|
||||
AdvancedFilterDto,
|
||||
AdvancedFilterResponse,
|
||||
} from './dto/advanced-filter.dto';
|
||||
|
||||
@ApiTags('Reports')
|
||||
@Controller('reports')
|
||||
@@ -68,12 +71,13 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get filtered ads with comprehensive filtering options',
|
||||
description: 'Retrieve ads based on multiple filter criteria including date range, ad type, user type, status, location, and categories'
|
||||
description:
|
||||
'Retrieve ads based on multiple filter criteria including date range, ad type, user type, status, location, and categories',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Successfully retrieved filtered ads',
|
||||
type: AdvancedFilterResponse
|
||||
type: AdvancedFilterResponse,
|
||||
})
|
||||
async getFilteredAds(@Query() filters: AdvancedFilterDto) {
|
||||
return this.reportsService.getFilteredAds(filters);
|
||||
@@ -83,11 +87,12 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get statistics for filtered ads',
|
||||
description: 'Get comprehensive statistics including breakdowns by status, type, location, category, and user type'
|
||||
description:
|
||||
'Get comprehensive statistics including breakdowns by status, type, location, category, and user type',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Successfully retrieved filtered ad statistics'
|
||||
description: 'Successfully retrieved filtered ad statistics',
|
||||
})
|
||||
async getFilteredAdStats(@Query() filters: AdvancedFilterDto) {
|
||||
return this.reportsService.getFilteredAdStats(filters);
|
||||
@@ -97,11 +102,12 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get categories for dropdown population',
|
||||
description: 'Retrieve all categories (main, subcategories) for use in filter dropdowns'
|
||||
description:
|
||||
'Retrieve all categories (main, subcategories) for use in filter dropdowns',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Successfully retrieved categories'
|
||||
description: 'Successfully retrieved categories',
|
||||
})
|
||||
async getCategoriesForFilters() {
|
||||
return this.reportsService.getCategoriesForFilters();
|
||||
@@ -111,11 +117,12 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get available user types for filtering',
|
||||
description: 'Retrieve all unique user types (posted by) values for filter dropdown'
|
||||
description:
|
||||
'Retrieve all unique user types (posted by) values for filter dropdown',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Successfully retrieved user types'
|
||||
description: 'Successfully retrieved user types',
|
||||
})
|
||||
async getUserTypesForFilters() {
|
||||
return this.reportsService.getUserTypesForFilters();
|
||||
@@ -125,11 +132,11 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get locations for filtering',
|
||||
description: 'Retrieve all unique states and cities for location filters'
|
||||
description: 'Retrieve all unique states and cities for location filters',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Successfully retrieved locations'
|
||||
description: 'Successfully retrieved locations',
|
||||
})
|
||||
async getLocationsForFilters() {
|
||||
return this.reportsService.getLocationsForFilters();
|
||||
@@ -139,16 +146,16 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Export filtered ads data',
|
||||
description: 'Export ads data based on filters to CSV format'
|
||||
description: 'Export ads data based on filters to CSV format',
|
||||
})
|
||||
@ApiQuery({ name: 'format', enum: ['csv', 'excel'], required: false })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Successfully exported filtered ads data'
|
||||
description: 'Successfully exported filtered ads data',
|
||||
})
|
||||
async exportFilteredAds(
|
||||
@Query() filters: AdvancedFilterDto,
|
||||
@Query('format') format: 'csv' | 'excel' = 'csv'
|
||||
@Query('format') format: 'csv' | 'excel' = 'csv',
|
||||
) {
|
||||
return this.reportsService.exportFilteredAds(filters, format);
|
||||
}
|
||||
@@ -161,20 +168,25 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get user registration reports',
|
||||
description: 'Get user registration statistics with date range and period grouping'
|
||||
description:
|
||||
'Get user registration statistics with date range and period grouping',
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
|
||||
@ApiQuery({
|
||||
name: 'period',
|
||||
enum: ['daily', 'weekly', 'monthly'],
|
||||
required: false,
|
||||
})
|
||||
async getUserRegistrationReport(
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string,
|
||||
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily'
|
||||
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
|
||||
) {
|
||||
const dateRange: ReportDateRange = {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
period
|
||||
period,
|
||||
};
|
||||
return this.reportsService.getUserRegistrationReport(dateRange);
|
||||
}
|
||||
@@ -183,7 +195,7 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get active vs inactive users',
|
||||
description: 'Get breakdown of active vs inactive users with percentages'
|
||||
description: 'Get breakdown of active vs inactive users with percentages',
|
||||
})
|
||||
async getActiveVsInactiveUsersReport() {
|
||||
return this.reportsService.getActiveVsInactiveUsersReport();
|
||||
@@ -193,20 +205,24 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get user login activity report',
|
||||
description: 'Get user activity patterns based on profile updates'
|
||||
description: 'Get user activity patterns based on profile updates',
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
|
||||
@ApiQuery({
|
||||
name: 'period',
|
||||
enum: ['daily', 'weekly', 'monthly'],
|
||||
required: false,
|
||||
})
|
||||
async getUserLoginActivityReport(
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string,
|
||||
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily'
|
||||
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
|
||||
) {
|
||||
const dateRange: ReportDateRange = {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
period
|
||||
period,
|
||||
};
|
||||
return this.reportsService.getUserLoginActivityReport(dateRange);
|
||||
}
|
||||
@@ -215,7 +231,7 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get user views per listing category',
|
||||
description: 'Get estimated user engagement by category'
|
||||
description: 'Get estimated user engagement by category',
|
||||
})
|
||||
async getUserViewsByCategoryReport() {
|
||||
return this.reportsService.getUserViewsByCategoryReport();
|
||||
@@ -227,22 +243,27 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get admin activity report',
|
||||
description: 'Get comprehensive admin activity breakdown with approvals, rejections, etc.'
|
||||
description:
|
||||
'Get comprehensive admin activity breakdown with approvals, rejections, etc.',
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
|
||||
@ApiQuery({
|
||||
name: 'period',
|
||||
enum: ['daily', 'weekly', 'monthly'],
|
||||
required: false,
|
||||
})
|
||||
@ApiQuery({ name: 'adminId', required: false, type: String })
|
||||
async getAdminActivityReport(
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string,
|
||||
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
|
||||
@Query('adminId') adminId?: string
|
||||
@Query('adminId') adminId?: string,
|
||||
) {
|
||||
const dateRange: ReportDateRange = {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
period
|
||||
period,
|
||||
};
|
||||
return this.reportsService.getAdminActivityReport(dateRange, adminId);
|
||||
}
|
||||
@@ -251,17 +272,17 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get admin user-wise activity report',
|
||||
description: 'Get activity breakdown for each admin user'
|
||||
description: 'Get activity breakdown for each admin user',
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: true, type: String })
|
||||
async getAdminUserWiseActivityReport(
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string
|
||||
@Query('endDate') endDate: string,
|
||||
) {
|
||||
const dateRange: ReportDateRange = {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate)
|
||||
endDate: new Date(endDate),
|
||||
};
|
||||
return this.reportsService.getAdminUserWiseActivityReport(dateRange);
|
||||
}
|
||||
@@ -270,17 +291,17 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get admin activity by category',
|
||||
description: 'Get admin actions breakdown by category'
|
||||
description: 'Get admin actions breakdown by category',
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: true, type: String })
|
||||
async getAdminActivityByCategoryReport(
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string
|
||||
@Query('endDate') endDate: string,
|
||||
) {
|
||||
const dateRange: ReportDateRange = {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate)
|
||||
endDate: new Date(endDate),
|
||||
};
|
||||
return this.reportsService.getAdminActivityByCategoryReport(dateRange);
|
||||
}
|
||||
@@ -291,18 +312,22 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get comprehensive listing analytics',
|
||||
description: 'Get detailed listing performance analytics including images, categories, approval times'
|
||||
description:
|
||||
'Get detailed listing performance analytics including images, categories, approval times',
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: false, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: false, type: String })
|
||||
async getListingAnalyticsReport(
|
||||
@Query('startDate') startDate?: string,
|
||||
@Query('endDate') endDate?: string
|
||||
@Query('endDate') endDate?: string,
|
||||
) {
|
||||
const dateRange = startDate && endDate ? {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate)
|
||||
} : undefined;
|
||||
const dateRange =
|
||||
startDate && endDate
|
||||
? {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
}
|
||||
: undefined;
|
||||
return this.reportsService.getListingAnalyticsReport(dateRange);
|
||||
}
|
||||
|
||||
@@ -310,7 +335,7 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get active listings count by category',
|
||||
description: 'Get count of active published listings grouped by category'
|
||||
description: 'Get count of active published listings grouped by category',
|
||||
})
|
||||
async getActiveListingsByCategory() {
|
||||
return this.reportsService.getActiveListingsByCategory();
|
||||
@@ -320,7 +345,7 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get approval time analytics',
|
||||
description: 'Get detailed approval time statistics for all ads'
|
||||
description: 'Get detailed approval time statistics for all ads',
|
||||
})
|
||||
async getApprovalTimeReport() {
|
||||
return this.reportsService.getApprovalTimeReport();
|
||||
@@ -330,7 +355,7 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get listings by user report',
|
||||
description: 'Get listing count and details grouped by user who posted'
|
||||
description: 'Get listing count and details grouped by user who posted',
|
||||
})
|
||||
async getListingsByUserReport() {
|
||||
return this.reportsService.getListingsByUserReport();
|
||||
@@ -342,20 +367,24 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get payment transaction report',
|
||||
description: 'Get comprehensive payment and revenue analytics'
|
||||
description: 'Get comprehensive payment and revenue analytics',
|
||||
})
|
||||
@ApiQuery({ name: 'startDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'endDate', required: true, type: String })
|
||||
@ApiQuery({ name: 'period', enum: ['daily', 'weekly', 'monthly'], required: false })
|
||||
@ApiQuery({
|
||||
name: 'period',
|
||||
enum: ['daily', 'weekly', 'monthly'],
|
||||
required: false,
|
||||
})
|
||||
async getPaymentTransactionReport(
|
||||
@Query('startDate') startDate: string,
|
||||
@Query('endDate') endDate: string,
|
||||
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily'
|
||||
@Query('period') period: 'daily' | 'weekly' | 'monthly' = 'daily',
|
||||
) {
|
||||
const dateRange: ReportDateRange = {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
period
|
||||
period,
|
||||
};
|
||||
return this.reportsService.getPaymentTransactionReport(dateRange);
|
||||
}
|
||||
@@ -364,7 +393,7 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get revenue by product type',
|
||||
description: 'Get revenue breakdown by Line Ads, Poster Ads, and Video Ads'
|
||||
description: 'Get revenue breakdown by Line Ads, Poster Ads, and Video Ads',
|
||||
})
|
||||
async getRevenueByProduct() {
|
||||
return this.reportsService.getRevenueByProduct();
|
||||
@@ -374,7 +403,7 @@ export class ReportsController {
|
||||
@Roles(Role.SUPER_ADMIN, Role.EDITOR, Role.REVIEWER)
|
||||
@ApiOperation({
|
||||
summary: 'Get revenue by category',
|
||||
description: 'Get revenue breakdown by ad categories'
|
||||
description: 'Get revenue breakdown by ad categories',
|
||||
})
|
||||
async getRevenueByCategoryReport() {
|
||||
return this.reportsService.getRevenueByCategoryReport();
|
||||
|
||||
@@ -18,19 +18,19 @@ import { MainCategory } from 'src/category/entities/main-category.entity';
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
LineAd,
|
||||
PosterAd,
|
||||
VideoAd,
|
||||
User,
|
||||
Customer,
|
||||
Admin,
|
||||
Payment,
|
||||
AdComment,
|
||||
MainCategory
|
||||
LineAd,
|
||||
PosterAd,
|
||||
VideoAd,
|
||||
User,
|
||||
Customer,
|
||||
Admin,
|
||||
Payment,
|
||||
AdComment,
|
||||
MainCategory,
|
||||
]),
|
||||
LineAdModule,
|
||||
PosterAdModule,
|
||||
VideoAdModule
|
||||
VideoAdModule,
|
||||
],
|
||||
controllers: [ReportsController],
|
||||
providers: [ReportsService],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,22 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
import { IsNotEmpty, IsEmail, IsEnum, IsOptional } from 'class-validator';
|
||||
import { IsNotEmpty, IsEmail, IsEnum, IsOptional, IsBoolean } from 'class-validator';
|
||||
import { Role } from 'src/common/enums/role.enum';
|
||||
|
||||
export class UpdateUserDto {
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
name?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
email: string;
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
email_verified?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
phone_verified?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Role } from 'src/common/enums/role.enum';
|
||||
import { BaseEntity } from 'src/common/types/base-entity';
|
||||
import { Column, Entity, JoinColumn, OneToOne } from 'typeorm';
|
||||
import { Column, Entity, JoinColumn, OneToOne, OneToMany } from 'typeorm';
|
||||
import { Admin } from './admin.entity';
|
||||
import { Customer } from './customer.entity';
|
||||
import { Otp } from 'src/auth/entities/otp.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User extends BaseEntity {
|
||||
@@ -27,9 +28,18 @@ export class User extends BaseEntity {
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
email_verified: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
phone_verified: boolean;
|
||||
|
||||
@OneToOne(() => Admin, (admin) => admin.user, { nullable: true })
|
||||
admin: Admin;
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.user, { nullable: true })
|
||||
customer: Customer;
|
||||
|
||||
@OneToMany(() => Otp, (otp) => otp.user, {nullable:true})
|
||||
otps: Otp[];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
ConflictException,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Inject,
|
||||
forwardRef,
|
||||
} from '@nestjs/common';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
@@ -54,6 +56,10 @@ export class UserService {
|
||||
// Optionally create admin/customer profile if specified by role
|
||||
if (user.role === Role.USER) {
|
||||
await this.customerRepo.save(this.customerRepo.create({ user: newUser }));
|
||||
|
||||
// Send verification instructions (don't send actual OTPs during registration)
|
||||
console.log(`User registered: ${newUser.email} / ${newUser.phone_number}`);
|
||||
console.log('Please use /auth/send-verification-otp to verify your account');
|
||||
} else {
|
||||
await this.adminRepo.save(this.adminRepo.create({ user: newUser }));
|
||||
}
|
||||
@@ -101,6 +107,7 @@ export class UserService {
|
||||
proof,
|
||||
});
|
||||
await manager.save(Customer, customer);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -100,4 +100,4 @@ export class VideoAd extends BaseEntity {
|
||||
@OneToOne(() => AdPosition, (position) => position.videoAd)
|
||||
@JoinColumn()
|
||||
position: AdPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export class VideoAdController {
|
||||
@Param('id') id: string,
|
||||
@Body() updateVideoAdDto: UpdateVideoAdDto,
|
||||
) {
|
||||
return this.videoAdService.updateAdByUser(user.sub, id, updateVideoAdDto);
|
||||
return this.videoAdService.updateAdByAdmin(user.sub, id, updateVideoAdDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
|
||||
@@ -14,7 +14,7 @@ import { AdPositionModule } from 'src/ad-position/ad-position.module';
|
||||
CategoryModule,
|
||||
ImageModule,
|
||||
UserModule,
|
||||
AdPositionModule
|
||||
AdPositionModule,
|
||||
],
|
||||
controllers: [VideoAdController],
|
||||
providers: [VideoAdService],
|
||||
|
||||
@@ -166,9 +166,12 @@ export class VideoAdService {
|
||||
stateId?: number;
|
||||
pageType: PageType;
|
||||
}) {
|
||||
// Get today's date in local timezone
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
const todayISOString = today.toISOString().split('T')[0]; // 'YYYY-MM-DD'
|
||||
const todayISOString = today.getFullYear() + '-' +
|
||||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(today.getDate()).padStart(2, '0');
|
||||
|
||||
const where: FindOptionsWhere<VideoAd> = {
|
||||
status: AdStatus.PUBLISHED,
|
||||
isActive: true,
|
||||
@@ -176,8 +179,8 @@ export class VideoAdService {
|
||||
pageType: pageType,
|
||||
},
|
||||
};
|
||||
|
||||
if (categoryId) {
|
||||
console.log(categoryId);
|
||||
where.mainCategory = { id: categoryId };
|
||||
}
|
||||
if (cityId) {
|
||||
@@ -186,37 +189,59 @@ export class VideoAdService {
|
||||
if (stateId) {
|
||||
where.sid = stateId;
|
||||
}
|
||||
// Fetch all published and active ads
|
||||
const lineAds = await this.videoAdRepo.find({
|
||||
|
||||
// Fetch all published and active ads with all necessary relations
|
||||
const videoAds = await this.videoAdRepo.find({
|
||||
where,
|
||||
relations: ['position'],
|
||||
relations: ['customer', 'customer.user', 'image', 'position', 'mainCategory', 'categoryOne', 'categoryTwo', 'categoryThree'],
|
||||
});
|
||||
|
||||
// Filter in JS for today's date in the dates array
|
||||
const filteredAds = lineAds.filter(
|
||||
(ad) =>
|
||||
Array.isArray(ad.dates) &&
|
||||
ad.dates.some((dateStr) => {
|
||||
const dateOnly = dateStr.split('T')[0];
|
||||
return dateOnly === todayISOString;
|
||||
}),
|
||||
);
|
||||
// Filter ads that should be displayed today
|
||||
const filteredAds = videoAds.filter((ad) => {
|
||||
if (!Array.isArray(ad.dates) || ad.dates.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ad.dates.some((dateStr) => {
|
||||
try {
|
||||
// Handle both ISO string and date-only formats
|
||||
const adDate = new Date(dateStr);
|
||||
const adDateString = adDate.getFullYear() + '-' +
|
||||
String(adDate.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(adDate.getDate()).padStart(2, '0');
|
||||
|
||||
return adDateString === todayISOString;
|
||||
} catch (error) {
|
||||
console.error('Error parsing date:', dateStr, error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Return cleaned up ad data
|
||||
return filteredAds.map((ad) => {
|
||||
const {
|
||||
payment,
|
||||
customer,
|
||||
cid,
|
||||
sid,
|
||||
sequenceNumber,
|
||||
orderId,
|
||||
status,
|
||||
comments,
|
||||
dates,
|
||||
isActive,
|
||||
cid,
|
||||
sid,
|
||||
...serializedAd
|
||||
} = ad;
|
||||
return serializedAd;
|
||||
|
||||
return {
|
||||
...serializedAd,
|
||||
// Include essential customer info without sensitive data
|
||||
customerName: customer?.user?.name || null,
|
||||
// Include location for display
|
||||
cityId: cid,
|
||||
stateId: sid,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -526,7 +551,9 @@ export class VideoAdService {
|
||||
if (filters?.userType) {
|
||||
query.andWhere('ad.postedBy = :userType', { userType: filters.userType });
|
||||
} else if (filters?.userTypes && filters.userTypes.length > 0) {
|
||||
query.andWhere('ad.postedBy IN (:...userTypes)', { userTypes: filters.userTypes });
|
||||
query.andWhere('ad.postedBy IN (:...userTypes)', {
|
||||
userTypes: filters.userTypes,
|
||||
});
|
||||
}
|
||||
|
||||
// Location filters
|
||||
@@ -545,16 +572,24 @@ export class VideoAdService {
|
||||
|
||||
// Category filters
|
||||
if (filters?.mainCategoryId) {
|
||||
query.andWhere('mainCategory.id = :mainCategoryId', { mainCategoryId: filters.mainCategoryId });
|
||||
query.andWhere('mainCategory.id = :mainCategoryId', {
|
||||
mainCategoryId: filters.mainCategoryId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryOneId) {
|
||||
query.andWhere('categoryOne.id = :categoryOneId', { categoryOneId: filters.categoryOneId });
|
||||
query.andWhere('categoryOne.id = :categoryOneId', {
|
||||
categoryOneId: filters.categoryOneId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryTwoId) {
|
||||
query.andWhere('categoryTwo.id = :categoryTwoId', { categoryTwoId: filters.categoryTwoId });
|
||||
query.andWhere('categoryTwo.id = :categoryTwoId', {
|
||||
categoryTwoId: filters.categoryTwoId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryThreeId) {
|
||||
query.andWhere('categoryThree.id = :categoryThreeId', { categoryThreeId: filters.categoryThreeId });
|
||||
query.andWhere('categoryThree.id = :categoryThreeId', {
|
||||
categoryThreeId: filters.categoryThreeId,
|
||||
});
|
||||
}
|
||||
if (filters?.categoryId) {
|
||||
query.andWhere(
|
||||
@@ -565,7 +600,9 @@ export class VideoAdService {
|
||||
|
||||
// Customer filter
|
||||
if (filters?.customerId) {
|
||||
query.andWhere('customer.id = :customerId', { customerId: filters.customerId });
|
||||
query.andWhere('customer.id = :customerId', {
|
||||
customerId: filters.customerId,
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
|
||||
Reference in New Issue
Block a user