diff --git a/src/contacts/contacts.controller.ts b/src/contacts/contacts.controller.ts new file mode 100644 index 0000000..b65d741 --- /dev/null +++ b/src/contacts/contacts.controller.ts @@ -0,0 +1,78 @@ +import { Body, Controller, Delete, Get, HttpCode, IsArray, IsOptional, IsString, NotFoundException, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { and, desc, eq, isNull } from 'drizzle-orm'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { db } from '../db/client'; +import { contacts } from '../db/schema'; + +class CreateContactDto { + @IsString() fullName!: string; + @IsOptional() @IsString() email?: string; + @IsOptional() @IsString() phone?: string; + @IsOptional() @IsString() role?: string; + @IsOptional() @IsString() organizationId?: string; + @IsOptional() @IsString() linkedinUrl?: string; + @IsOptional() @IsString() notes?: string; + @IsOptional() @IsArray() tags?: string[]; + @IsOptional() @IsString() source?: string; + @IsOptional() @IsString() consentStatus?: string; +} + +class UpdateContactDto { + @IsOptional() @IsString() fullName?: string; + @IsOptional() @IsString() email?: string; + @IsOptional() @IsString() phone?: string; + @IsOptional() @IsString() role?: string; + @IsOptional() @IsString() organizationId?: string; + @IsOptional() @IsString() linkedinUrl?: string; + @IsOptional() @IsString() notes?: string; + @IsOptional() @IsArray() tags?: string[]; + @IsOptional() @IsString() consentStatus?: string; +} + +@Controller('contacts') +export class ContactsController { + @Get() + async list(@CurrentSession() session: AuthenticatedSession, @Query('limit') limitStr?: string) { + const limit = Math.min(Math.max(1, parseInt(limitStr ?? '200', 10) || 200), 500); + return db.query.contacts.findMany({ + where: and(eq(contacts.tenantId, session.tenantId), isNull(contacts.deletedAt)), + orderBy: desc(contacts.createdAt), + limit, + }); + } + + @Post() + async create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateContactDto) { + const [contact] = await db.insert(contacts).values({ + ...dto, + tenantId: session.tenantId, + tags: dto.tags ?? [], + }).returning(); + return contact; + } + + @Get(':id') + async getOne(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + const contact = await db.query.contacts.findFirst({ + where: and(eq(contacts.id, id), eq(contacts.tenantId, session.tenantId), isNull(contacts.deletedAt)), + }); + if (!contact) throw new NotFoundException('Contact not found'); + return contact; + } + + @Patch(':id') + async update(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContactDto) { + const [updated] = await db.update(contacts).set({ ...dto, updatedAt: new Date() }) + .where(and(eq(contacts.id, id), eq(contacts.tenantId, session.tenantId))).returning(); + if (!updated) throw new NotFoundException('Contact not found'); + return updated; + } + + @Delete(':id') + @HttpCode(204) + async remove(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + await db.update(contacts).set({ deletedAt: new Date() }) + .where(and(eq(contacts.id, id), eq(contacts.tenantId, session.tenantId))); + } +}