From 23c83ffbeef5c8f7b1687035737bc75dd94e51c1 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 5 Oct 2026 11:56:49 +0330 Subject: [PATCH] Add per-user saved column visibility for tables Co-authored-by: Cursor --- .../migration.sql | 22 ++ apps/api/prisma/schema.prisma | 17 ++ apps/api/src/app.module.ts | 2 + .../src/table-prefs/table-prefs.controller.ts | 92 +++++++++ .../api/src/table-prefs/table-prefs.module.ts | 9 + apps/web/src/components/TableColumnsMenu.vue | 177 ++++++++++++++++ .../src/components/pipeline/PipelineList.vue | 9 +- apps/web/src/lib/tableColumns.ts | 189 ++++++++++++++++++ apps/web/src/main.ts | 2 + apps/web/src/styles/main.css | 6 + apps/web/src/views/AttachmentsView.vue | 6 +- apps/web/src/views/AuditLogsView.vue | 6 +- apps/web/src/views/ChequesView.vue | 6 +- apps/web/src/views/CommissionsView.vue | 6 +- apps/web/src/views/ContactsView.vue | 6 +- apps/web/src/views/InventoryView.vue | 9 +- apps/web/src/views/InvoicesView.vue | 6 +- apps/web/src/views/LeadsView.vue | 4 +- apps/web/src/views/LoyaltyClubView.vue | 6 +- apps/web/src/views/MessageAutomationsView.vue | 6 +- apps/web/src/views/ProductsSettingsView.vue | 6 +- apps/web/src/views/PublicApiSettingsView.vue | 6 +- apps/web/src/views/SalesForecastView.vue | 6 +- apps/web/src/views/SmsCampaignReportView.vue | 6 +- apps/web/src/views/TahesabSettingsView.vue | 11 +- apps/web/src/views/TeamSettingsView.vue | 6 +- apps/web/src/views/TicketsView.vue | 8 +- apps/web/src/views/VoipSettingsView.vue | 6 +- .../web/src/views/WooCommerceSettingsView.vue | 6 +- 29 files changed, 624 insertions(+), 23 deletions(-) create mode 100644 apps/api/prisma/migrations/20261005090000_user_table_preferences/migration.sql create mode 100644 apps/api/src/table-prefs/table-prefs.controller.ts create mode 100644 apps/api/src/table-prefs/table-prefs.module.ts create mode 100644 apps/web/src/components/TableColumnsMenu.vue create mode 100644 apps/web/src/lib/tableColumns.ts diff --git a/apps/api/prisma/migrations/20261005090000_user_table_preferences/migration.sql b/apps/api/prisma/migrations/20261005090000_user_table_preferences/migration.sql new file mode 100644 index 0000000..d410471 --- /dev/null +++ b/apps/api/prisma/migrations/20261005090000_user_table_preferences/migration.sql @@ -0,0 +1,22 @@ +-- CreateTable +CREATE TABLE IF NOT EXISTS "UserTablePreference" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "tableKey" TEXT NOT NULL, + "hiddenColumns" TEXT[] DEFAULT ARRAY[]::TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserTablePreference_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "UserTablePreference_userId_organizationId_tableKey_key" ON "UserTablePreference"("userId", "organizationId", "tableKey"); +CREATE INDEX IF NOT EXISTS "UserTablePreference_userId_organizationId_idx" ON "UserTablePreference"("userId", "organizationId"); + +-- AddForeignKey +DO $$ BEGIN + ALTER TABLE "UserTablePreference" ADD CONSTRAINT "UserTablePreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 71ff819..4f10a99 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -264,6 +264,7 @@ model User { billingAccount BillingAccount? auditLogs AuditLog[] @relation("AuditLogActor") savedViews SavedView[] @relation("SavedViewOwner") + tablePreferences UserTablePreference[] assignedProjectCards ProjectCard[] @relation("ProjectCardAssignees") teamMemberships TeamMember[] reservationAssignments ReservationAppointment[] @relation("ReservationAssignee") @@ -1712,6 +1713,22 @@ model AuditLog { } /// فیلترهای ذخیره‌شده برای لیست‌ها و کاریزها +/// ستون‌های مخفی هر جدول برای هر کاربر (به‌ازای هر سازمان) +model UserTablePreference { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + organizationId String + tableKey String + /// ستون جدیدی که بعداً اضافه شود به‌صورت پیش‌فرض نمایش داده می‌شود + hiddenColumns String[] @default([]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, organizationId, tableKey]) + @@index([userId, organizationId]) +} + model SavedView { id String @id @default(cuid()) organizationId String diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index f62abfd..a29010d 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -37,6 +37,7 @@ import { PlatformModule } from './platform/platform.module'; import { BillingModule } from './billing/billing.module'; import { AuditLogsModule } from './audit-logs/audit-logs.module'; import { SavedViewsModule } from './saved-views/saved-views.module'; +import { TablePrefsModule } from './table-prefs/table-prefs.module'; import { PublicApiModule } from './public-api/public-api.module'; import { SmsCampaignsModule } from './sms-campaigns/sms-campaigns.module'; import { TeamsModule } from './teams/teams.module'; @@ -89,6 +90,7 @@ import { ConversationsModule } from './conversations/conversations.module'; TahesabModule, AuditLogsModule, SavedViewsModule, + TablePrefsModule, PublicApiModule, SmsCampaignsModule, TrackingModule, diff --git a/apps/api/src/table-prefs/table-prefs.controller.ts b/apps/api/src/table-prefs/table-prefs.controller.ts new file mode 100644 index 0000000..dcd8987 --- /dev/null +++ b/apps/api/src/table-prefs/table-prefs.controller.ts @@ -0,0 +1,92 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Put, + UseGuards, +} from '@nestjs/common'; +import { + ArrayMaxSize, + IsArray, + IsString, + Matches, + MaxLength, +} from 'class-validator'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { CurrentUser } from '../auth/current-user.decorator'; +import { PrismaService } from '../prisma/prisma.service'; + +const TABLE_KEY_RE = /^[a-z0-9_.-]{1,80}$/i; + +class SaveTablePrefDto { + @IsArray() + @ArrayMaxSize(80) + @IsString({ each: true }) + @MaxLength(80, { each: true }) + hiddenColumns!: string[]; +} + +class TableKeyParam { + @Matches(TABLE_KEY_RE) + key!: string; +} + +type Actor = { userId: string; organizationId: string }; + +@Controller('table-prefs') +@UseGuards(JwtAuthGuard) +export class TablePrefsController { + constructor(private readonly prisma: PrismaService) {} + + @Get() + async list(@CurrentUser() user: Actor) { + const rows = await this.prisma.userTablePreference.findMany({ + where: { userId: user.userId, organizationId: user.organizationId }, + select: { tableKey: true, hiddenColumns: true }, + }); + const out: Record = {}; + for (const row of rows) out[row.tableKey] = row.hiddenColumns; + return out; + } + + @Put(':key') + async save( + @CurrentUser() user: Actor, + @Param() params: TableKeyParam, + @Body() dto: SaveTablePrefDto, + ) { + const hiddenColumns = [...new Set(dto.hiddenColumns.map((c) => c.trim()).filter(Boolean))]; + const row = await this.prisma.userTablePreference.upsert({ + where: { + userId_organizationId_tableKey: { + userId: user.userId, + organizationId: user.organizationId, + tableKey: params.key, + }, + }, + create: { + userId: user.userId, + organizationId: user.organizationId, + tableKey: params.key, + hiddenColumns, + }, + update: { hiddenColumns }, + select: { tableKey: true, hiddenColumns: true }, + }); + return row; + } + + @Delete(':key') + async reset(@CurrentUser() user: Actor, @Param() params: TableKeyParam) { + await this.prisma.userTablePreference.deleteMany({ + where: { + userId: user.userId, + organizationId: user.organizationId, + tableKey: params.key, + }, + }); + return { ok: true }; + } +} diff --git a/apps/api/src/table-prefs/table-prefs.module.ts b/apps/api/src/table-prefs/table-prefs.module.ts new file mode 100644 index 0000000..92401ad --- /dev/null +++ b/apps/api/src/table-prefs/table-prefs.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module'; +import { TablePrefsController } from './table-prefs.controller'; + +@Module({ + imports: [AuthModule], + controllers: [TablePrefsController], +}) +export class TablePrefsModule {} diff --git a/apps/web/src/components/TableColumnsMenu.vue b/apps/web/src/components/TableColumnsMenu.vue new file mode 100644 index 0000000..a5e0f47 --- /dev/null +++ b/apps/web/src/components/TableColumnsMenu.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/apps/web/src/components/pipeline/PipelineList.vue b/apps/web/src/components/pipeline/PipelineList.vue index ad7e7fc..1e2aea3 100644 --- a/apps/web/src/components/pipeline/PipelineList.vue +++ b/apps/web/src/components/pipeline/PipelineList.vue @@ -1,5 +1,6 @@