Add store-item Excel export/import for bulk price and stock updates.

Dashboard can download grouped product variants and re-upload to update price and stock. Also expose Content-Disposition for downloads and keep related product/customer API docs in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-19 01:02:43 +03:30
co-authored by Cursor
parent 4a5780e74d
commit 7277817b6e
17 changed files with 1553 additions and 52 deletions
+19
View File
@@ -18,6 +18,8 @@ type CustomerListRow = {
cellNumber: string;
firstName: string | null;
lastName: string | null;
firstNameEn: string | null;
lastNameEn: string | null;
email: string | null;
createdAt: Date;
isEnabled: boolean;
@@ -63,7 +65,10 @@ export class CustomersService {
AND (
u.first_name ILIKE ${nameLike}
OR u.last_name ILIKE ${nameLike}
OR u.first_name_en ILIKE ${nameLike}
OR u.last_name_en ILIKE ${nameLike}
OR (COALESCE(u.first_name, '') || ' ' || COALESCE(u.last_name, '')) ILIKE ${nameLike}
OR (COALESCE(u.first_name_en, '') || ' ' || COALESCE(u.last_name_en, '')) ILIKE ${nameLike}
)
` : Prisma.empty}
${cellLike ? Prisma.sql`AND u.cell_number ILIKE ${cellLike}` : Prisma.empty}
@@ -76,6 +81,8 @@ export class CustomersService {
u.cell_number AS "cellNumber",
u.first_name AS "firstName",
u.last_name AS "lastName",
u.first_name_en AS "firstNameEn",
u.last_name_en AS "lastNameEn",
u.email AS "email",
COALESCE(bc.created_at, bu.created_at) AS "createdAt",
COALESCE(bc.is_enabled, TRUE) AS "isEnabled",
@@ -109,6 +116,8 @@ export class CustomersService {
cellNumber: user.cellNumber,
firstName: user.firstName,
lastName: user.lastName,
firstNameEn: user.firstNameEn,
lastNameEn: user.lastNameEn,
email: user.email,
label: this.formatLabel(user),
createdAt: user.createdAt.toISOString(),
@@ -335,6 +344,8 @@ export class CustomersService {
cellNumber: user.cellNumber,
firstName: user.firstName,
lastName: user.lastName,
firstNameEn: user.firstNameEn,
lastNameEn: user.lastNameEn,
email: user.email,
label: this.formatLabel(user),
createdAt: membership.link.createdAt.toISOString(),
@@ -375,6 +386,8 @@ export class CustomersService {
const hasProfileUpdate =
dto.firstName !== undefined ||
dto.lastName !== undefined ||
dto.firstNameEn !== undefined ||
dto.lastNameEn !== undefined ||
dto.email !== undefined ||
dto.cellNumber !== undefined;
@@ -395,6 +408,10 @@ export class CustomersService {
data: {
firstName: dto.firstName?.trim(),
lastName: dto.lastName?.trim(),
firstNameEn:
dto.firstNameEn !== undefined ? dto.firstNameEn.trim() || null : undefined,
lastNameEn:
dto.lastNameEn !== undefined ? dto.lastNameEn.trim() || null : undefined,
email: dto.email !== undefined ? dto.email.trim() || null : undefined,
cellNumber: dto.cellNumber?.trim(),
},
@@ -416,6 +433,8 @@ export class CustomersService {
cellNumber: user.cellNumber,
firstName: user.firstName,
lastName: user.lastName,
firstNameEn: user.firstNameEn,
lastNameEn: user.lastNameEn,
email: user.email,
label: this.formatLabel(user),
isEnabled: refreshed.isEnabled,
+24 -1
View File
@@ -1,4 +1,11 @@
import { IsBoolean, IsOptional, IsString, MinLength } from 'class-validator';
import {
IsBoolean,
IsOptional,
IsString,
MaxLength,
MinLength,
ValidateIf,
} from 'class-validator';
export class UpdateCustomerDto {
@IsOptional()
@@ -8,13 +15,29 @@ export class UpdateCustomerDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(100)
firstName?: string;
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(100)
lastName?: string;
@IsOptional()
@IsString()
@ValidateIf((_, value) => typeof value === 'string' && value.trim().length > 0)
@MinLength(2)
@MaxLength(100)
firstNameEn?: string;
@IsOptional()
@IsString()
@ValidateIf((_, value) => typeof value === 'string' && value.trim().length > 0)
@MinLength(2)
@MaxLength(100)
lastNameEn?: string;
@IsOptional()
@IsString()
email?: string;