chore: company and industy related logix

This commit is contained in:
Mahyargdz
2025-05-17 10:45:53 +03:30
parent 1efc3a2795
commit 479c9c390a
10 changed files with 117 additions and 77 deletions
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; 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"; import { CompanyMessage } from "../../../common/enums/message.enum";
@@ -10,8 +10,7 @@ export class CreateCompanyProductDto {
title: string; title: string;
@IsNotEmpty({ message: CompanyMessage.PRODUCT_IMAGES_REQUIRED }) @IsNotEmpty({ message: CompanyMessage.PRODUCT_IMAGES_REQUIRED })
@IsArray({ message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_ARRAY }) @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_URL })
@IsUrl({ protocols: ["http", "https"] }, { each: true, message: CompanyMessage.PRODUCT_IMAGES_MUST_BE_URL }) @ApiProperty({ description: "تصاویر محصول", example: "https://example.com/image1.jpg" })
@ApiProperty({ description: "تصاویر محصول", example: ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] }) imageUrl: string;
imagesUrl: string[];
} }
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; 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"; import { CompanyMessage } from "../../../common/enums/message.enum";
@@ -10,8 +10,7 @@ export class CreateCompanyServiceDto {
title: string; title: string;
@IsNotEmpty({ message: CompanyMessage.SERVICE_IMAGES_REQUIRED }) @IsNotEmpty({ message: CompanyMessage.SERVICE_IMAGES_REQUIRED })
@IsArray({ message: CompanyMessage.SERVICE_IMAGES_MUST_BE_ARRAY }) @IsUrl({ protocols: ["http", "https"] }, { message: CompanyMessage.SERVICE_IMAGES_MUST_BE_URL })
@IsUrl({ protocols: ["http", "https"] }, { each: true, message: CompanyMessage.SERVICE_IMAGES_MUST_BE_URL }) @ApiProperty({ description: "تصاویر خدمت", example: "https://example.com/image1.jpg" })
@ApiProperty({ description: "تصاویر خدمت", example: ["https://example.com/image1.jpg", "https://example.com/image2.jpg"] }) imageUrl: string;
imagesUrl: string[];
} }
@@ -8,8 +8,8 @@ export class CompanyProduct extends BaseEntity {
@Property({ type: "varchar", length: 250, nullable: false }) @Property({ type: "varchar", length: 250, nullable: false })
title!: string; title!: string;
@Property({ type: "array", nullable: false }) @Property({ type: "varchar", length: 250, nullable: false })
imagesUrl!: string[]; imageUrl!: string;
@ManyToOne(() => Company, { deleteRule: "cascade" }) @ManyToOne(() => Company, { deleteRule: "cascade" })
company!: Company; company!: Company;
@@ -8,8 +8,8 @@ export class CompanyService extends BaseEntity {
@Property({ type: "varchar", length: 250, nullable: false }) @Property({ type: "varchar", length: 250, nullable: false })
title!: string; title!: string;
@Property({ type: "array", nullable: false }) @Property({ type: "varchar", length: 250, nullable: false })
imagesUrl!: string[]; imageUrl!: string;
@ManyToOne(() => Company, { deleteRule: "cascade" }) @ManyToOne(() => Company, { deleteRule: "cascade" })
company!: Company; company!: Company;
@@ -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 { CompanyProduct } from "./company-product.entity";
import { CompanyService } from "./company-service.entity"; import { CompanyService } from "./company-service.entity";
@@ -52,9 +52,12 @@ export class Company extends BaseEntity {
@OneToMany(() => CompanyService, (service) => service.company, { cascade: [Cascade.ALL] }) @OneToMany(() => CompanyService, (service) => service.company, { cascade: [Cascade.ALL] })
services = new Collection<CompanyService>(this); services = new Collection<CompanyService>(this);
@OneToMany(() => Invoice, (invoice) => invoice.company) @OneToMany(() => Invoice, (invoice) => invoice.company, { cascade: [Cascade.ALL] })
invoices = new Collection<Invoice>(this); invoices = new Collection<Invoice>(this);
@Formula((alias) => `(SELECT COUNT(*)::int FROM invoice i WHERE i.company_id = ${alias}.id)`, { persist: false })
invoiceCount?: number;
//----------------------------------- //-----------------------------------
@ManyToOne(() => Industry, { deleteRule: "restrict" }) @ManyToOne(() => Industry, { deleteRule: "restrict" })
@@ -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 { PaginationUtils } from "../../utils/providers/pagination.utils";
import { CompanyListQueryDto } from "../DTO/company-list-query.dto"; import { CompanyListQueryDto } from "../DTO/company-list-query.dto";
@@ -9,57 +9,52 @@ export class CompanyRepository extends EntityRepository<Company> {
async getCompaniesListForAdmin(queryDto: CompanyListQueryDto) { async getCompaniesListForAdmin(queryDto: CompanyListQueryDto) {
const { limit, skip } = PaginationUtils(queryDto); const { limit, skip } = PaginationUtils(queryDto);
const qb = this.createQueryBuilder("company") const where: FilterQuery<Company> = {
.select([ deletedAt: null,
"company.id", };
"company.name",
"company.status", if (queryDto.status) {
"company.isActive", where.status = queryDto.status;
"company.createdAt", }
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.id",
"industry.title", "industry.title",
"user.id", "user.id",
"user.firstName", "user.firstName",
"user.lastName", "user.lastName",
"(SELECT COUNT(*) FROM invoice WHERE invoice.company_id = company.id) as invoiceCount", "user.fullName",
]) ],
.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),
// }));
} }
} }
@@ -47,14 +47,14 @@ export class CompaniesService {
const products = createDto.products.map((productData) => { const products = createDto.products.map((productData) => {
const product = new CompanyProduct(); const product = new CompanyProduct();
product.title = productData.title; product.title = productData.title;
product.imagesUrl = productData.imagesUrl; product.imageUrl = productData.imageUrl;
return product; return product;
}); });
const services = createDto.services.map((serviceData) => { const services = createDto.services.map((serviceData) => {
const service = new CompanyService(); const service = new CompanyService();
service.title = serviceData.title; service.title = serviceData.title;
service.imagesUrl = serviceData.imagesUrl; service.imageUrl = serviceData.imageUrl;
return service; return service;
}); });
@@ -87,10 +87,12 @@ export class CompaniesService {
try { try {
await em.begin(); 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); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND);
const updatedCompany = em.assign(company, updateDto); const updatedCompany = em.assign(company, rest);
if (updateDto.industryId) { if (updateDto.industryId) {
const industry = await em.findOne(Industry, { id: updateDto.industryId, deletedAt: null }); 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 (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.flush();
await em.commit(); await em.commit();
return { return {
message: CompanyMessage.UPDATED_SUCCESSFULLY, message: CompanyMessage.UPDATED_SUCCESSFULLY,
updatedCompany, company: updatedCompany.name,
}; };
} catch (error) { } catch (error) {
await em.rollback(); await em.rollback();
@@ -194,7 +232,10 @@ export class CompaniesService {
} }
//----------------------------------- //-----------------------------------
private async findCompanyById(id: string) { 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); if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND);
return company; return company;
} }
@@ -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 { BaseEntity } from "../../../common/entities/base.entity";
import { Company } from "../../companies/entities/company.entity"; import { Company } from "../../companies/entities/company.entity";
@@ -15,6 +15,9 @@ export class Industry extends BaseEntity {
@Property({ type: "boolean", default: true }) @Property({ type: "boolean", default: true })
isActive!: boolean & Opt; 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) @OneToMany(() => Company, (company) => company.industry)
companies = new Collection<Company>(this); companies = new Collection<Company>(this);
@@ -9,12 +9,7 @@ export class IndustryRepository extends EntityRepository<Industry> {
async getIndustriesListForAdmin(queryDto: IndustryListQueryDto) { async getIndustriesListForAdmin(queryDto: IndustryListQueryDto) {
const { limit, skip } = PaginationUtils(queryDto); const { limit, skip } = PaginationUtils(queryDto);
const queryBuilder = this.createQueryBuilder("i") const queryBuilder = this.createQueryBuilder("i").where({ deletedAt: null }).orderBy({ createdAt: "DESC" }).limit(limit).offset(skip);
.leftJoinAndSelect("i.companies", "c")
.where({ deletedAt: null })
.orderBy({ createdAt: "DESC" })
.limit(limit)
.offset(skip);
if (queryDto.q) { if (queryDto.q) {
queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } }); queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } });
@@ -38,6 +38,11 @@ export class User extends BaseEntity {
@Property({ type: "boolean", default: false }) @Property({ type: "boolean", default: false })
emailVerified!: boolean & Opt; emailVerified!: boolean & Opt;
@Property({ persist: false })
get fullName(): string | null {
return `${this.firstName} ${this.lastName}`;
}
//----------------------------------- //-----------------------------------
@ManyToOne(() => Role, { deleteRule: "restrict" }) @ManyToOne(() => Role, { deleteRule: "restrict" })