diff --git a/src/modules/companies/DTO/create-company-product.dto.ts b/src/modules/companies/DTO/create-company-product.dto.ts index eb2ae83..8acbb7e 100644 --- a/src/modules/companies/DTO/create-company-product.dto.ts +++ b/src/modules/companies/DTO/create-company-product.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsNotEmpty, IsString, IsUrl } from "class-validator"; +import { IsNotEmpty, IsString, IsUrl } from "class-validator"; import { CompanyMessage } from "../../../common/enums/message.enum"; @@ -10,8 +10,7 @@ export class CreateCompanyProductDto { title: string; @IsNotEmpty({ message: CompanyMessage.PRODUCT_IMAGES_REQUIRED }) - @IsArray({ message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_ARRAY }) - @IsUrl({ protocols: ["http", "https"] }, { each: true, message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_URL }) - @ApiProperty({ description: "تصاویر محصول", example: ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] }) - imagesUrl: string[]; + @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_URL }) + @ApiProperty({ description: "تصاویر محصول", example: "https://example.com/image1.jpg" }) + imageUrl: string; } diff --git a/src/modules/companies/DTO/create-company-service.dto.ts b/src/modules/companies/DTO/create-company-service.dto.ts index 8cbdd0b..ff86e3b 100644 --- a/src/modules/companies/DTO/create-company-service.dto.ts +++ b/src/modules/companies/DTO/create-company-service.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsNotEmpty, IsString, IsUrl } from "class-validator"; +import { IsNotEmpty, IsString, IsUrl } from "class-validator"; import { CompanyMessage } from "../../../common/enums/message.enum"; @@ -10,8 +10,7 @@ export class CreateCompanyServiceDto { title: string; @IsNotEmpty({ message: CompanyMessage.SERVICE_IMAGES_REQUIRED }) - @IsArray({ message: CompanyMessage.SERVICE_IMAGES_MUST_BE_ARRAY }) - @IsUrl({ protocols: ["http", "https"] }, { each: true, message: CompanyMessage.SERVICE_IMAGES_MUST_BE_URL }) - @ApiProperty({ description: "تصاویر خدمت", example: ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] }) - imagesUrl: string[]; + @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.SERVICE_IMAGES_MUST_BE_URL }) + @ApiProperty({ description: "تصاویر خدمت", example: "https://example.com/image1.jpg" }) + imageUrl: string; } diff --git a/src/modules/companies/entities/company-product.entity.ts b/src/modules/companies/entities/company-product.entity.ts index 14ec9a0..52a475d 100644 --- a/src/modules/companies/entities/company-product.entity.ts +++ b/src/modules/companies/entities/company-product.entity.ts @@ -8,8 +8,8 @@ export class CompanyProduct extends BaseEntity { @Property({ type: "varchar", length: 250, nullable: false }) title!: string; - @Property({ type: "array", nullable: false }) - imagesUrl!: string[]; + @Property({ type: "varchar", length: 250, nullable: false }) + imageUrl!: string; @ManyToOne(() => Company, { deleteRule: "cascade" }) company!: Company; diff --git a/src/modules/companies/entities/company-service.entity.ts b/src/modules/companies/entities/company-service.entity.ts index ed4c9ca..e0004a6 100644 --- a/src/modules/companies/entities/company-service.entity.ts +++ b/src/modules/companies/entities/company-service.entity.ts @@ -8,8 +8,8 @@ export class CompanyService extends BaseEntity { @Property({ type: "varchar", length: 250, nullable: false }) title!: string; - @Property({ type: "array", nullable: false }) - imagesUrl!: string[]; + @Property({ type: "varchar", length: 250, nullable: false }) + imageUrl!: string; @ManyToOne(() => Company, { deleteRule: "cascade" }) company!: Company; diff --git a/src/modules/companies/entities/company.entity.ts b/src/modules/companies/entities/company.entity.ts index 87d72b1..be07aa8 100644 --- a/src/modules/companies/entities/company.entity.ts +++ b/src/modules/companies/entities/company.entity.ts @@ -1,4 +1,4 @@ -import { Cascade, Collection, Entity, EntityRepositoryType, Enum, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core"; +import { Cascade, Collection, Entity, EntityRepositoryType, Enum, Formula, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core"; import { CompanyProduct } from "./company-product.entity"; import { CompanyService } from "./company-service.entity"; @@ -52,9 +52,12 @@ export class Company extends BaseEntity { @OneToMany(() => CompanyService, (service) => service.company, { cascade: [Cascade.ALL] }) services = new Collection(this); - @OneToMany(() => Invoice, (invoice) => invoice.company) + @OneToMany(() => Invoice, (invoice) => invoice.company, { cascade: [Cascade.ALL] }) invoices = new Collection(this); + @Formula((alias) => `(SELECT COUNT(*)::int FROM invoice i WHERE i.company_id = ${alias}.id)`, { persist: false }) + invoiceCount?: number; + //----------------------------------- @ManyToOne(() => Industry, { deleteRule: "restrict" }) diff --git a/src/modules/companies/repositories/company.repository.ts b/src/modules/companies/repositories/company.repository.ts index 314e514..3cb9697 100644 --- a/src/modules/companies/repositories/company.repository.ts +++ b/src/modules/companies/repositories/company.repository.ts @@ -1,4 +1,4 @@ -import { EntityRepository } from "@mikro-orm/postgresql"; +import { EntityRepository, FilterQuery } from "@mikro-orm/postgresql"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { CompanyListQueryDto } from "../DTO/company-list-query.dto"; @@ -9,57 +9,52 @@ export class CompanyRepository extends EntityRepository { async getCompaniesListForAdmin(queryDto: CompanyListQueryDto) { const { limit, skip } = PaginationUtils(queryDto); - const qb = this.createQueryBuilder("company") - .select([ - "company.id", - "company.name", - "company.status", - "company.isActive", - "company.createdAt", + const where: FilterQuery = { + deletedAt: null, + }; + + if (queryDto.status) { + where.status = queryDto.status; + } + if (queryDto.isActive !== undefined) { + where.isActive = queryDto.isActive === 1; + } + if (queryDto.q) { + where.name = { $ilike: `%${queryDto.q}%` }; + } + if (queryDto.industryId) { + where.industry = queryDto.industryId; + } + if (queryDto.registrationDate) { + where.createdAt = { + $gte: queryDto.registrationDate, + $lte: queryDto.registrationDate, + }; + } + if (queryDto.name) { + where.name = { $ilike: `%${queryDto.name}%` }; + } + + return this.findAndCount(where, { + populate: ["industry", "user"], + limit, + offset: skip, + orderBy: { createdAt: "DESC" }, + fields: [ + "id", + "name", + "isActive", + "dateOfEstablishment", + "status", + "invoiceCount", + "createdAt", "industry.id", "industry.title", "user.id", "user.firstName", "user.lastName", - "(SELECT COUNT(*) FROM invoice WHERE invoice.company_id = company.id) as invoiceCount", - ]) - .leftJoin("company.industry", "industry") - .leftJoin("company.user", "user") - .where({ - deletedAt: null, - ...(queryDto.status && { status: queryDto.status }), - ...(queryDto.isActive !== undefined && { isActive: queryDto.isActive === 1 }), - }); - - if (queryDto.q) { - qb.andWhere({ name: { $ilike: `%${queryDto.q}%` } }); - } - - if (queryDto.industryId) { - qb.andWhere({ industry: { id: queryDto.industryId } }); - } - - if (queryDto.registrationDate) { - qb.andWhere({ - createdAt: { - $gte: queryDto.registrationDate, - $lte: queryDto.registrationDate, - }, - }); - } - - if (queryDto.name) { - qb.andWhere({ name: { $ilike: `%${queryDto.name}%` } }); - } - - qb.orderBy({ createdAt: "DESC" }).limit(limit).offset(skip); - - return qb.getResultAndCount(); - - // // Transform the result to include invoiceCount as a number - // const transformedCompanies = companies.map((company) => ({ - // ...company, - // invoiceCount: parseInt(company.invoiceCount, 10), - // })); + "user.fullName", + ], + }); } } diff --git a/src/modules/companies/services/companies.service.ts b/src/modules/companies/services/companies.service.ts index ab7c27f..aee12ff 100644 --- a/src/modules/companies/services/companies.service.ts +++ b/src/modules/companies/services/companies.service.ts @@ -47,14 +47,14 @@ export class CompaniesService { const products = createDto.products.map((productData) => { const product = new CompanyProduct(); product.title = productData.title; - product.imagesUrl = productData.imagesUrl; + product.imageUrl = productData.imageUrl; return product; }); const services = createDto.services.map((serviceData) => { const service = new CompanyService(); service.title = serviceData.title; - service.imagesUrl = serviceData.imagesUrl; + service.imageUrl = serviceData.imageUrl; return service; }); @@ -87,10 +87,12 @@ export class CompaniesService { try { await em.begin(); - const company = await em.findOne(Company, { id, deletedAt: null }, { populate: ["user"] }); + const { services, products, ...rest } = updateDto; + + const company = await em.findOne(Company, { id, deletedAt: null }, { populate: ["user", "services", "products"] }); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); - const updatedCompany = em.assign(company, updateDto); + const updatedCompany = em.assign(company, rest); if (updateDto.industryId) { const industry = await em.findOne(Industry, { id: updateDto.industryId, deletedAt: null }); @@ -121,12 +123,48 @@ export class CompaniesService { if (Object.keys(userUpdateData).length > 0) await this.usersService.updateUser(company.user.id, userUpdateData, em); + if (services && services.length > 0) { + // Remove all old services from the database + for (const oldService of company.services.getItems()) { + em.remove(oldService); + } + await em.flush(); // flush removals before adding new + + // Add new services + for (const service of services) { + const serviceEntity = em.create(CompanyService, { + title: service.title, + imageUrl: service.imageUrl, + company: company, + }); + updatedCompany.services.add(serviceEntity); + } + } + + if (products && products.length > 0) { + // Remove all old products from the database + for (const oldProduct of company.products.getItems()) { + em.remove(oldProduct); + } + await em.flush(); // flush removals before adding new + + // Add new products + for (const product of products) { + const productEntity = em.create(CompanyProduct, { + title: product.title, + imageUrl: product.imageUrl, + company: company, + }); + updatedCompany.products.add(productEntity); + } + } + await em.flush(); await em.commit(); return { message: CompanyMessage.UPDATED_SUCCESSFULLY, - updatedCompany, + company: updatedCompany.name, }; } catch (error) { await em.rollback(); @@ -194,7 +232,10 @@ export class CompaniesService { } //----------------------------------- private async findCompanyById(id: string) { - const company = await this.companyRepository.findOne({ id, deletedAt: null }); + const company = await this.companyRepository.findOne( + { id, deletedAt: null }, + { populate: ["user", "industry", "services", "products"] }, + ); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND); return company; } diff --git a/src/modules/industries/entities/industry.entity.ts b/src/modules/industries/entities/industry.entity.ts index 2a8d9f0..6c494ba 100644 --- a/src/modules/industries/entities/industry.entity.ts +++ b/src/modules/industries/entities/industry.entity.ts @@ -1,4 +1,4 @@ -import { Collection, Entity, EntityRepositoryType, OneToMany, Opt, Property } from "@mikro-orm/core"; +import { Collection, Entity, EntityRepositoryType, Formula, OneToMany, Opt, Property } from "@mikro-orm/core"; import { BaseEntity } from "../../../common/entities/base.entity"; import { Company } from "../../companies/entities/company.entity"; @@ -15,6 +15,9 @@ export class Industry extends BaseEntity { @Property({ type: "boolean", default: true }) isActive!: boolean & Opt; + @Formula((alias) => `(SELECT COUNT(*)::int FROM company c WHERE c.industry_id = ${alias}.id)`, { persist: false }) + companiesCount?: number; + @OneToMany(() => Company, (company) => company.industry) companies = new Collection(this); diff --git a/src/modules/industries/repositories/industry.repository.ts b/src/modules/industries/repositories/industry.repository.ts index ed8c1c4..693a4e8 100644 --- a/src/modules/industries/repositories/industry.repository.ts +++ b/src/modules/industries/repositories/industry.repository.ts @@ -9,12 +9,7 @@ export class IndustryRepository extends EntityRepository { async getIndustriesListForAdmin(queryDto: IndustryListQueryDto) { const { limit, skip } = PaginationUtils(queryDto); - const queryBuilder = this.createQueryBuilder("i") - .leftJoinAndSelect("i.companies", "c") - .where({ deletedAt: null }) - .orderBy({ createdAt: "DESC" }) - .limit(limit) - .offset(skip); + const queryBuilder = this.createQueryBuilder("i").where({ deletedAt: null }).orderBy({ createdAt: "DESC" }).limit(limit).offset(skip); if (queryDto.q) { queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } }); diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index c10debd..f9e97c9 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -38,6 +38,11 @@ export class User extends BaseEntity { @Property({ type: "boolean", default: false }) emailVerified!: boolean & Opt; + @Property({ persist: false }) + get fullName(): string | null { + return `${this.firstName} ${this.lastName}`; + } + //----------------------------------- @ManyToOne(() => Role, { deleteRule: "restrict" })