feat: Full petmaster CRM with NestJS API and React frontend

- Client management (CRUD with JSONB custom fields)
- Pet profiles
- Service catalog with per-client pricing
- Booking calendar with recurring appointments
- Invoicing with Stripe integration
- Real-time messaging (WebSocket)
- Self-booking portal
- Staff assignment
- Docker multi-stage builds
- Prisma schema + seed
This commit is contained in:
Robert Perez
2026-07-11 00:17:49 +00:00
commit 8d742518f8
65 changed files with 3365 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
import {
Controller, Get, Post, Put, Delete, Param, Body, Query,
ParseUUIDPipe, BadRequestException,
} from '@nestjs/common';
import { ClientsService } from './clients.service';
import { CreateClientDto, UpdateClientDto, ListClientsQueryDto } from './dto/client.dto';
import { Client } from '@prisma/client';
@Controller('clients')
export class ClientsController {
constructor(private readonly clientsService: ClientsService) {}
@Post()
async create(@Body() dto: CreateClientDto) {
return this.clientsService.create(dto);
}
@Get()
async findAll(@Query() query: ListClientsQueryDto) {
return this.clientsService.findAll(query);
}
@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.clientsService.findOne(id);
}
@Put(':id')
async update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateClientDto) {
return this.clientsService.update(id, dto);
}
@Delete(':id')
async remove(@Param('id', ParseUUIDPipe) id: string) {
return this.clientsService.remove(id);
}
@Put(':id/custom-fields')
async updateCustomFields(
@Param('id', ParseUUIDPipe) id: string,
@Body() customFields: Record<string, any>,
) {
return this.clientsService.updateCustomFields(id, customFields);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ClientsController } from './clients.controller';
import { ClientsService } from './clients.service';
@Module({
controllers: [ClientsController],
providers: [ClientsService],
exports: [ClientsService],
})
export class ClientsModule {}
+62
View File
@@ -0,0 +1,62 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateClientDto, UpdateClientDto, ListClientsQueryDto } from './dto/client.dto';
@Injectable()
export class ClientsService {
constructor(private prisma: PrismaService) {}
async create(dto: CreateClientDto) {
return this.prisma.client.create({ data: dto });
}
async findAll(query: ListClientsQueryDto) {
const { page = 1, limit = 20, search, isActive } = query;
const skip = (page - 1) * limit;
const where: any = {};
if (isActive !== undefined) where.isActive = isActive === 'true';
if (search) {
where.OR = [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
{ phone: { contains: search, mode: 'insensitive' } },
];
}
const [items, total] = await Promise.all([
this.prisma.client.findMany({ where, skip, take: +limit, orderBy: { createdAt: 'desc' } }),
this.prisma.client.count({ where }),
]);
return { items, total, page, limit: +limit };
}
async findOne(id: string) {
const client = await this.prisma.client.findUnique({
where: { id },
include: { pets: true },
});
if (!client) throw new NotFoundException('Client not found');
return client;
}
async update(id: string, dto: UpdateClientDto) {
await this.prisma.client.findUniqueOrThrow({ where: { id } });
return this.prisma.client.update({ where: { id }, data: dto });
}
async remove(id: string) {
await this.prisma.client.findUniqueOrThrow({ where: { id } });
return this.prisma.client.delete({ where: { id } });
}
async updateCustomFields(id: string, customFields: Record<string, any>) {
const client = await this.prisma.client.findUniqueOrThrow({ where: { id } });
return this.prisma.client.update({
where: { id },
data: { customFields: { ...client.customFields, ...customFields } as any },
});
}
}
+74
View File
@@ -0,0 +1,74 @@
import { IsEmail, IsOptional, IsString, IsBoolean } from 'class-validator';
export class CreateClientDto {
@IsString()
firstName: string;
@IsString()
lastName: string;
@IsEmail()
email: string;
@IsString()
@IsOptional()
phone?: string;
@IsString()
@IsOptional()
address?: string;
@IsString()
@IsOptional()
notes?: string;
@IsOptional()
customFields?: Record<string, any>;
}
export class UpdateClientDto {
@IsString()
@IsOptional()
firstName?: string;
@IsString()
@IsOptional()
lastName?: string;
@IsEmail()
@IsOptional()
email?: string;
@IsString()
@IsOptional()
phone?: string;
@IsString()
@IsOptional()
address?: string;
@IsString()
@IsOptional()
notes?: string;
@IsOptional()
customFields?: Record<string, any>;
}
export class ListClientsQueryDto {
@IsOptional()
@IsString()
search?: string;
@IsOptional()
@IsBoolean()
isActive?: boolean;
@IsOptional()
@IsString()
page?: string;
@IsOptional()
@IsString()
limit?: string;
}