Editing of the project's Kariz page
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { ImportContactsResult } from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
parseAmount,
|
||||
readMappedRows,
|
||||
writeXlsx,
|
||||
} from '../common/excel';
|
||||
import { ProductsService } from './products.service';
|
||||
|
||||
const PRODUCT_COLUMNS = [
|
||||
{ header: '_type', key: '_type', width: 12 },
|
||||
{ header: 'نوع', key: 'kind', width: 12 },
|
||||
{ header: 'نام محصول', key: 'name', width: 28 },
|
||||
{ header: 'کد کالا', key: 'sku', width: 16 },
|
||||
{ header: 'دستهبندی', key: 'category', width: 16 },
|
||||
{ header: 'واحد', key: 'unit', width: 10 },
|
||||
{ header: 'موجودی', key: 'stockQty', width: 12 },
|
||||
{ header: 'برند', key: 'brand', width: 14 },
|
||||
{ header: 'مدل', key: 'model', width: 14 },
|
||||
{ header: 'کشور', key: 'country', width: 12 },
|
||||
{ header: 'قیمت واحد', key: 'unitPrice', width: 14 },
|
||||
{ header: 'ارز', key: 'currency', width: 10 },
|
||||
{ header: 'وضعیت', key: 'active', width: 12 },
|
||||
{ header: 'برچسب', key: 'tags', width: 16 },
|
||||
{ header: 'توضیحات', key: 'description', width: 32 },
|
||||
];
|
||||
|
||||
const PRODUCT_ALIASES: Record<string, string> = {
|
||||
_type: '_type',
|
||||
type: '_type',
|
||||
نوع: 'kind',
|
||||
ناممحصول: 'name',
|
||||
نام: 'name',
|
||||
نامکالا: 'name',
|
||||
کدکالا: 'sku',
|
||||
sku: 'sku',
|
||||
کد: 'sku',
|
||||
دستهبندی: 'category',
|
||||
دسته: 'category',
|
||||
واحد: 'unit',
|
||||
موجودی: 'stockQty',
|
||||
برند: 'brand',
|
||||
مدل: 'model',
|
||||
کشور: 'country',
|
||||
قیمتواحد: 'unitPrice',
|
||||
قیمت: 'unitPrice',
|
||||
ارز: 'currency',
|
||||
وضعیت: 'active',
|
||||
فعال: 'active',
|
||||
برچسب: 'tags',
|
||||
توضیحات: 'description',
|
||||
};
|
||||
|
||||
function kindLabel(kind: string) {
|
||||
return kind === 'service' ? 'خدمت' : 'کالا';
|
||||
}
|
||||
|
||||
function parseKind(raw?: string): 'product' | 'service' {
|
||||
return /خدمت|service/i.test(raw ?? '') ? 'service' : 'product';
|
||||
}
|
||||
|
||||
function parseActive(raw?: string) {
|
||||
const v = (raw ?? '').trim();
|
||||
if (!v) return true;
|
||||
return !/غیرفعال|inactive|خیر|no|0/i.test(v);
|
||||
}
|
||||
|
||||
function parseTags(raw?: string) {
|
||||
return (raw ?? '')
|
||||
.split(/[،,|#]+/)
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 20);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ProductsExcelService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly products: ProductsService,
|
||||
) {}
|
||||
|
||||
async export(organizationId: string, q?: string): Promise<Buffer> {
|
||||
const items = await this.products.list(organizationId, { q });
|
||||
return writeXlsx(
|
||||
'Product',
|
||||
PRODUCT_COLUMNS,
|
||||
items.map((p) => ({
|
||||
_type: 'Product',
|
||||
kind: kindLabel(p.kind),
|
||||
name: p.name,
|
||||
sku: p.sku ?? '',
|
||||
category: p.category ?? '',
|
||||
unit: p.unit ?? '',
|
||||
stockQty: p.stockQty ?? '',
|
||||
brand: p.brand ?? '',
|
||||
model: p.model ?? '',
|
||||
country: p.country ?? '',
|
||||
unitPrice: p.unitPrice ?? '',
|
||||
currency: p.currency,
|
||||
active: p.isActive ? 'فعال' : 'غیرفعال',
|
||||
tags: (p.tags ?? []).join('، '),
|
||||
description: p.description ?? '',
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async import(
|
||||
organizationId: string,
|
||||
file: Express.Multer.File,
|
||||
): Promise<ImportContactsResult> {
|
||||
const rows = await readMappedRows(file, PRODUCT_ALIASES);
|
||||
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i]!;
|
||||
const rowNum = i + 2;
|
||||
if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'product') {
|
||||
continue;
|
||||
}
|
||||
const name = (row.name || '').trim();
|
||||
if (name.length < 1) {
|
||||
result.errors.push({ row: rowNum, message: 'نام محصول الزامی است' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const sku = row.sku?.trim() || undefined;
|
||||
const duplicate = await this.prisma.product.findFirst({
|
||||
where: {
|
||||
organizationId,
|
||||
OR: [
|
||||
...(sku ? [{ sku: { equals: sku, mode: 'insensitive' as const } }] : []),
|
||||
{ name: { equals: name, mode: 'insensitive' } },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (duplicate) {
|
||||
result.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.products.create(organizationId, {
|
||||
name,
|
||||
kind: parseKind(row.kind),
|
||||
sku,
|
||||
category: row.category || undefined,
|
||||
unit: row.unit || undefined,
|
||||
stockQty: parseAmount(row.stockQty),
|
||||
brand: row.brand || undefined,
|
||||
model: row.model || undefined,
|
||||
country: row.country || undefined,
|
||||
tags: parseTags(row.tags),
|
||||
unitPrice: parseAmount(row.unitPrice),
|
||||
currency: row.currency || undefined,
|
||||
isActive: parseActive(row.active),
|
||||
description: row.description || undefined,
|
||||
});
|
||||
result.created += 1;
|
||||
} catch (e) {
|
||||
result.errors.push({
|
||||
row: rowNum,
|
||||
message: e instanceof Error ? e.message : 'خطای ناشناخته',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,32 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import type { Response } from 'express';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import type { Actor } from '../common/record-scope';
|
||||
import { CreateProductDto, ListProductsQueryDto, UpdateProductDto } from './dto';
|
||||
import { ProductsService } from './products.service';
|
||||
import { ProductsExcelService } from './products-excel.service';
|
||||
import { brand } from '@mento/shared';
|
||||
|
||||
@Controller('products')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class ProductsController {
|
||||
constructor(private readonly productsService: ProductsService) {}
|
||||
constructor(
|
||||
private readonly productsService: ProductsService,
|
||||
private readonly excelService: ProductsExcelService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@@ -34,6 +47,36 @@ export class ProductsController {
|
||||
});
|
||||
}
|
||||
|
||||
@Get('export')
|
||||
async export(
|
||||
@CurrentUser() user: Actor,
|
||||
@Query('q') q: string | undefined,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
const buffer = await this.excelService.export(user.organizationId, q);
|
||||
res.set({
|
||||
'Content-Type':
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition': `attachment; filename="${brand.filePrefix}-products.xlsx"`,
|
||||
});
|
||||
return new StreamableFile(buffer);
|
||||
}
|
||||
|
||||
@Post('import')
|
||||
@Roles('owner', 'admin')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 15_000_000 },
|
||||
}),
|
||||
)
|
||||
import(
|
||||
@CurrentUser() user: Actor,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
) {
|
||||
return this.excelService.import(user.organizationId, file);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
getOne(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
|
||||
@@ -3,11 +3,12 @@ import { AuthModule } from '../auth/auth.module';
|
||||
import { InventoryModule } from '../inventory/inventory.module';
|
||||
import { ProductsController } from './products.controller';
|
||||
import { ProductsService } from './products.service';
|
||||
import { ProductsExcelService } from './products-excel.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, InventoryModule],
|
||||
controllers: [ProductsController],
|
||||
providers: [ProductsService],
|
||||
providers: [ProductsService, ProductsExcelService],
|
||||
exports: [ProductsService],
|
||||
})
|
||||
export class ProductsModule {}
|
||||
|
||||
Reference in New Issue
Block a user