From 48de552f3620cd24cf8a7d506790fe209ff62542 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 5 Oct 2026 20:00:21 +0330 Subject: [PATCH] Add row selection, bulk delete, row numbers and page size picker to tables; redesign person profile header Co-authored-by: Cursor --- apps/api/src/contacts/dto.ts | 10 + apps/api/src/contacts/people.controller.ts | 7 +- apps/api/src/contacts/people.service.ts | 13 + apps/web/src/components/ui/UiPagination.vue | 57 ++++- apps/web/src/lib/pageSize.ts | 25 ++ apps/web/src/lib/tableColumns.ts | 4 + apps/web/src/styles/main.css | 45 ++++ apps/web/src/views/ActivitiesView.vue | 34 ++- apps/web/src/views/AuditLogsView.vue | 5 +- apps/web/src/views/ContactsView.vue | 155 +++++++++++- apps/web/src/views/InvoicesView.vue | 9 +- apps/web/src/views/LoyaltyClubView.vue | 4 +- apps/web/src/views/PersonProfileView.vue | 255 ++++++++++++++++---- apps/web/src/views/ProductsSettingsView.vue | 15 +- apps/web/src/views/TahesabSettingsView.vue | 6 +- 15 files changed, 565 insertions(+), 79 deletions(-) create mode 100644 apps/web/src/lib/pageSize.ts diff --git a/apps/api/src/contacts/dto.ts b/apps/api/src/contacts/dto.ts index 8fb3f04..6bdeb58 100644 --- a/apps/api/src/contacts/dto.ts +++ b/apps/api/src/contacts/dto.ts @@ -1,5 +1,7 @@ import { Type } from 'class-transformer'; import { + ArrayMaxSize, + ArrayMinSize, IsArray, IsEmail, IsIn, @@ -15,6 +17,14 @@ import { } from 'class-validator'; import { PERSON_IMPORT_SOURCES, PERSON_MOODS } from '@mento/shared'; +export class BulkDeletePeopleDto { + @IsArray() + @ArrayMinSize(1) + @ArrayMaxSize(500) + @IsString({ each: true }) + ids!: string[]; +} + export class ListQueryDto { @IsOptional() @IsString() diff --git a/apps/api/src/contacts/people.controller.ts b/apps/api/src/contacts/people.controller.ts index b9c3b97..8827cc2 100644 --- a/apps/api/src/contacts/people.controller.ts +++ b/apps/api/src/contacts/people.controller.ts @@ -22,7 +22,7 @@ import { CurrentUser } from '../auth/current-user.decorator'; import type { Actor } from '../common/record-scope'; import { OrgModuleGuard } from '../billing/org-module.guard'; import { RequireOrgModule } from '../billing/org-module.decorator'; -import { CreatePersonDto, ListQueryDto, UpdatePersonDto } from './dto'; +import { BulkDeletePeopleDto, CreatePersonDto, ListQueryDto, UpdatePersonDto } from './dto'; import { PeopleService } from './people.service'; import { ContactsExcelService } from './contacts-excel.service'; import { brand } from '@mento/shared'; @@ -110,6 +110,11 @@ export class PeopleController { return this.peopleService.get(user, id); } + @Post('bulk-delete') + bulkDelete(@CurrentUser() user: Actor, @Body() dto: BulkDeletePeopleDto) { + return this.peopleService.removeMany(user, dto.ids); + } + @Post() create(@CurrentUser() user: Actor, @Body() dto: CreatePersonDto) { return this.peopleService.create(user, { diff --git a/apps/api/src/contacts/people.service.ts b/apps/api/src/contacts/people.service.ts index d51a376..375ce0c 100644 --- a/apps/api/src/contacts/people.service.ts +++ b/apps/api/src/contacts/people.service.ts @@ -309,6 +309,19 @@ export class PeopleService { await this.prisma.person.delete({ where: { id } }); } + async removeMany(actor: Actor, ids: string[]): Promise<{ deleted: number }> { + const unique = [...new Set(ids.filter(Boolean))]; + if (!unique.length) return { deleted: 0 }; + const res = await this.prisma.person.deleteMany({ + where: { + id: { in: unique }, + organizationId: actor.organizationId, + ...ownerWhere(actor), + }, + }); + return { deleted: res.count }; + } + private async ensureExists(actor: Actor, id: string) { const found = await this.prisma.person.findFirst({ where: { id, organizationId: actor.organizationId, ...ownerWhere(actor) }, diff --git a/apps/web/src/components/ui/UiPagination.vue b/apps/web/src/components/ui/UiPagination.vue index 92f3c10..16218c0 100644 --- a/apps/web/src/components/ui/UiPagination.vue +++ b/apps/web/src/components/ui/UiPagination.vue @@ -2,6 +2,7 @@ import { computed } from 'vue'; import UiButton from './UiButton.vue'; import UiIcon from '@/components/UiIcon.vue'; +import { toFaDigits } from '@/lib/jalali'; const props = withDefaults( defineProps<{ @@ -9,6 +10,8 @@ const props = withDefaults( totalPages?: number; totalItems?: number; pageSize?: number; + /** با مقداردهی، انتخاب «تعداد در صفحه» نمایش داده می‌شود */ + pageSizeOptions?: number[]; }>(), { page: 1, @@ -17,6 +20,7 @@ const props = withDefaults( const emit = defineEmits<{ 'update:page': [page: number]; + 'update:pageSize': [size: number]; change: [page: number]; }>(); @@ -28,22 +32,45 @@ const effectiveTotalPages = computed(() => { return 1; }); +const showSizePicker = computed(() => Boolean(props.pageSizeOptions?.length && props.pageSize)); + function goTo(p: number) { if (p < 1 || p > effectiveTotalPages.value || p === props.page) return; emit('update:page', p); emit('change', p); } + +function onSizeChange(e: Event) { + const size = Number((e.target as HTMLSelectElement).value); + if (!size || size === props.pageSize) return; + emit('update:pageSize', size); + if (props.page !== 1) { + emit('update:page', 1); + emit('change', 1); + } +}