diff --git a/backend/prisma/migrations/20260223210000_add_soft_delete_targets_corrections/migration.sql b/backend/prisma/migrations/20260223210000_add_soft_delete_targets_corrections/migration.sql new file mode 100644 index 0000000..21c1ec4 --- /dev/null +++ b/backend/prisma/migrations/20260223210000_add_soft_delete_targets_corrections/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable: add deleted_at column to client_targets +ALTER TABLE "client_targets" ADD COLUMN "deleted_at" TIMESTAMP(3); + +-- AlterTable: add deleted_at column to balance_corrections +ALTER TABLE "balance_corrections" ADD COLUMN "deleted_at" TIMESTAMP(3); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 5f8fc9e..83b49fb 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -100,11 +100,12 @@ model OngoingTimer { } model ClientTarget { - id String @id @default(uuid()) - weeklyHours Float @map("weekly_hours") - startDate DateTime @map("start_date") @db.Date // Always a Monday - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(uuid()) + weeklyHours Float @map("weekly_hours") + startDate DateTime @map("start_date") @db.Date // Always a Monday + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") userId String @map("user_id") @db.VarChar(255) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -120,12 +121,13 @@ model ClientTarget { } model BalanceCorrection { - id String @id @default(uuid()) - date DateTime @map("date") @db.Date + id String @id @default(uuid()) + date DateTime @map("date") @db.Date hours Float - description String? @db.VarChar(255) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + description String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") clientTargetId String @map("client_target_id") clientTarget ClientTarget @relation(fields: [clientTargetId], references: [id], onDelete: Cascade) diff --git a/backend/src/services/clientTarget.service.ts b/backend/src/services/clientTarget.service.ts index cf79a64..e2e0419 100644 --- a/backend/src/services/clientTarget.service.ts +++ b/backend/src/services/clientTarget.service.ts @@ -68,10 +68,10 @@ export interface ClientTargetWithBalance { export class ClientTargetService { async findAll(userId: string): Promise { const targets = await prisma.clientTarget.findMany({ - where: { userId, client: { deletedAt: null } }, + where: { userId, deletedAt: null, client: { deletedAt: null } }, include: { client: { select: { id: true, name: true } }, - corrections: { orderBy: { date: 'asc' } }, + corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } }, }, orderBy: { client: { name: 'asc' } }, }); @@ -81,10 +81,10 @@ export class ClientTargetService { async findById(id: string, userId: string) { return prisma.clientTarget.findFirst({ - where: { id, userId, client: { deletedAt: null } }, + where: { id, userId, deletedAt: null, client: { deletedAt: null } }, include: { client: { select: { id: true, name: true } }, - corrections: { orderBy: { date: 'asc' } }, + corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } }, }, }); } @@ -106,6 +106,18 @@ export class ClientTargetService { // Check for existing target (unique per user+client) const existing = await prisma.clientTarget.findFirst({ where: { userId, clientId: data.clientId } }); if (existing) { + if (existing.deletedAt !== null) { + // Reactivate the soft-deleted target with the new settings + const reactivated = await prisma.clientTarget.update({ + where: { id: existing.id }, + data: { deletedAt: null, weeklyHours: data.weeklyHours, startDate }, + include: { + client: { select: { id: true, name: true } }, + corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } }, + }, + }); + return this.computeBalance(reactivated); + } throw new BadRequestError('A target already exists for this client. Delete the existing one first or update it.'); } @@ -118,7 +130,7 @@ export class ClientTargetService { }, include: { client: { select: { id: true, name: true } }, - corrections: { orderBy: { date: 'asc' } }, + corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } }, }, }); @@ -148,7 +160,7 @@ export class ClientTargetService { data: updateData, include: { client: { select: { id: true, name: true } }, - corrections: { orderBy: { date: 'asc' } }, + corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } }, }, }); @@ -158,7 +170,10 @@ export class ClientTargetService { async delete(id: string, userId: string): Promise { const existing = await this.findById(id, userId); if (!existing) throw new NotFoundError('Client target not found'); - await prisma.clientTarget.delete({ where: { id } }); + await prisma.clientTarget.update({ + where: { id }, + data: { deletedAt: new Date() }, + }); } async addCorrection(targetId: string, userId: string, data: CreateCorrectionInput) { @@ -188,11 +203,14 @@ export class ClientTargetService { if (!target) throw new NotFoundError('Client target not found'); const correction = await prisma.balanceCorrection.findFirst({ - where: { id: correctionId, clientTargetId: targetId }, + where: { id: correctionId, clientTargetId: targetId, deletedAt: null }, }); if (!correction) throw new NotFoundError('Correction not found'); - await prisma.balanceCorrection.delete({ where: { id: correctionId } }); + await prisma.balanceCorrection.update({ + where: { id: correctionId }, + data: { deletedAt: new Date() }, + }); } private async computeBalance(target: { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index eceee0c..ea39d9e 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -152,6 +152,7 @@ export interface BalanceCorrection { hours: number; description: string | null; createdAt: string; + deletedAt: string | null; } export interface WeekBalance {