import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, IsNull } from 'typeorm'; import { ContentPiece, ContentStatus, ContentType } from '../entities/content-piece.entity'; import { CreateContentPieceDto, UpdateContentPieceDto } from '../dto'; import { TenantAwareService } from '@/shared/services/tenant-aware.service'; import { PaginationParams, PaginatedResult } from '@/shared/dto/pagination.dto'; @Injectable() export class ContentPieceService extends TenantAwareService { constructor( @InjectRepository(ContentPiece) private readonly contentPieceRepository: Repository, ) { super(contentPieceRepository); } async findAllPaginated( tenantId: string, pagination: PaginationParams & { projectId?: string; type?: ContentType; status?: ContentStatus; search?: string; }, ): Promise> { const { page = 1, limit = 20, sortBy = 'created_at', sortOrder = 'DESC', projectId, type, status, search, } = pagination; const queryBuilder = this.contentPieceRepository .createQueryBuilder('content') .leftJoinAndSelect('content.project', 'project') .leftJoinAndSelect('content.assignee', 'assignee') .where('content.tenant_id = :tenantId', { tenantId }) .andWhere('content.deleted_at IS NULL'); if (projectId) { queryBuilder.andWhere('content.project_id = :projectId', { projectId }); } if (type) { queryBuilder.andWhere('content.type = :type', { type }); } if (status) { queryBuilder.andWhere('content.status = :status', { status }); } if (search) { queryBuilder.andWhere( '(content.title ILIKE :search OR content.content ILIKE :search)', { search: `%${search}%` }, ); } queryBuilder.orderBy(`content.${sortBy}`, sortOrder); const total = await queryBuilder.getCount(); const totalPages = Math.ceil(total / limit); queryBuilder.skip((page - 1) * limit).take(limit); const data = await queryBuilder.getMany(); return { data, meta: { total, page, limit, totalPages, hasNextPage: page < totalPages, hasPreviousPage: page > 1, }, }; } async findById(tenantId: string, id: string): Promise { const contentPiece = await this.contentPieceRepository.findOne({ where: { id, tenant_id: tenantId, deleted_at: IsNull() }, relations: ['project', 'assignee'], }); if (!contentPiece) { throw new NotFoundException(`Content piece with ID ${id} not found`); } return contentPiece; } async findByProject(tenantId: string, projectId: string): Promise { return this.contentPieceRepository.find({ where: { tenant_id: tenantId, project_id: projectId, deleted_at: IsNull() }, relations: ['assignee'], order: { created_at: 'DESC' }, }); } async create(tenantId: string, dto: CreateContentPieceDto): Promise { const contentPiece = this.contentPieceRepository.create({ ...dto, tenant_id: tenantId, version: 1, }); return this.contentPieceRepository.save(contentPiece); } async update(tenantId: string, id: string, dto: UpdateContentPieceDto): Promise { const contentPiece = await this.findById(tenantId, id); // Increment version on content changes if (dto.content && dto.content !== contentPiece.content) { contentPiece.version += 1; } Object.assign(contentPiece, dto); return this.contentPieceRepository.save(contentPiece); } async updateStatus(tenantId: string, id: string, status: ContentStatus): Promise { const contentPiece = await this.findById(tenantId, id); contentPiece.status = status; if (status === ContentStatus.PUBLISHED) { contentPiece.published_at = new Date(); } return this.contentPieceRepository.save(contentPiece); } async assignTo(tenantId: string, id: string, userId: string | null): Promise { const contentPiece = await this.findById(tenantId, id); contentPiece.assigned_to = userId; return this.contentPieceRepository.save(contentPiece); } async schedule(tenantId: string, id: string, scheduledAt: Date): Promise { const contentPiece = await this.findById(tenantId, id); contentPiece.scheduled_at = scheduledAt; contentPiece.status = ContentStatus.SCHEDULED; return this.contentPieceRepository.save(contentPiece); } async publish(tenantId: string, id: string, publishedUrl?: string): Promise { const contentPiece = await this.findById(tenantId, id); contentPiece.status = ContentStatus.PUBLISHED; contentPiece.published_at = new Date(); if (publishedUrl) { contentPiece.published_url = publishedUrl; } return this.contentPieceRepository.save(contentPiece); } async softDelete(tenantId: string, id: string): Promise { const contentPiece = await this.findById(tenantId, id); contentPiece.deleted_at = new Date(); await this.contentPieceRepository.save(contentPiece); } async duplicate(tenantId: string, id: string): Promise { const original = await this.findById(tenantId, id); const duplicate = this.contentPieceRepository.create({ ...original, id: undefined, title: `${original.title} (Copy)`, status: ContentStatus.IDEA, published_at: null, published_url: null, scheduled_at: null, version: 1, created_at: undefined, updated_at: undefined, }); return this.contentPieceRepository.save(duplicate); } }