From 1333538097e63141b09879e489a3e5a34cecea14 Mon Sep 17 00:00:00 2001 From: Rishavraaj Date: Fri, 7 Aug 2026 17:55:52 +0530 Subject: [PATCH] feat(leads): add Revert to Lead action for converted leads - Backend: POST /leads/:id/revert resets status to replied, clears dealId/contactId - Frontend: LeadsList shows Revert button for converted leads, Convert button otherwise - Linked deal/contact are preserved on revert Co-Authored-By: Claude Sonnet 4.6 --- .../backend/src/app/leads/leads.controller.ts | 11 ++++ apps/backend/src/app/leads/leads.service.ts | 9 ++++ apps/web/src/components/Leads/LeadsList.tsx | 53 ++++++++++++------- apps/web/src/lib/api/leads.ts | 4 ++ 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/apps/backend/src/app/leads/leads.controller.ts b/apps/backend/src/app/leads/leads.controller.ts index d29f3424..ea4e67b6 100644 --- a/apps/backend/src/app/leads/leads.controller.ts +++ b/apps/backend/src/app/leads/leads.controller.ts @@ -90,4 +90,15 @@ export class LeadsController { ) { return this.leadsService.convert(id, organizationId); } + + @Post(':id/revert') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Revert converted lead back to replied status' }) + @ApiParam({ name: 'id', type: Number }) + revert( + @OrgId() organizationId: number, + @Param('id', ParseIntPipe) id: number, + ) { + return this.leadsService.revert(id, organizationId); + } } diff --git a/apps/backend/src/app/leads/leads.service.ts b/apps/backend/src/app/leads/leads.service.ts index 259cf6dd..2ff4a98a 100644 --- a/apps/backend/src/app/leads/leads.service.ts +++ b/apps/backend/src/app/leads/leads.service.ts @@ -59,6 +59,15 @@ export class LeadsService { return this.leadsRepository.delete(id); } + async revert(id: number, organizationId: number) { + await this.findById(id, organizationId); + return this.leadsRepository.update(id, { + status: 'replied', + dealId: null, + contactId: null, + }); + } + async convert(id: number, organizationId: number) { const lead = await this.findById(id, organizationId); diff --git a/apps/web/src/components/Leads/LeadsList.tsx b/apps/web/src/components/Leads/LeadsList.tsx index 780c0b51..9bc634eb 100644 --- a/apps/web/src/components/Leads/LeadsList.tsx +++ b/apps/web/src/components/Leads/LeadsList.tsx @@ -71,6 +71,15 @@ const LeadsList = () => { onError: () => toast.error('Failed to convert lead'), }); + const { mutate: revertLead } = useMutation({ + mutationFn: (id: number) => leadsApi.revert(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['leads'] }); + toast.success('Lead reverted'); + }, + onError: () => toast.error('Failed to revert lead'), + }); + const leads = leadsData?.pages.flatMap((p) => p.data) ?? []; const metadata = leadsData?.pages[0]?.metadata ?? []; const totalCount = leadsData?.pages[0]?.pagination?.total; @@ -79,27 +88,31 @@ const LeadsList = () => { () => ({ id: 'actions', header: 'Actions', - cell: ({ row }) => ( - - - setLeadToDelete(Number(row.original.id))} - disabled={isDeleting} - /> - - ), + cell: ({ row }) => { + const status = (row.original as unknown as Record)[ + 'status' + ] as string; + const id = Number(row.original.id); + return ( + + {status === 'converted' ? ( + + ) : ( + + )} + setLeadToDelete(id)} + disabled={isDeleting} + /> + + ); + }, }), - [convertLead, isDeleting], + [convertLead, revertLead, isDeleting], ); const icpProfileColumn: ColumnDef = useMemo( diff --git a/apps/web/src/lib/api/leads.ts b/apps/web/src/lib/api/leads.ts index 6a913b9b..0715c6c9 100644 --- a/apps/web/src/lib/api/leads.ts +++ b/apps/web/src/lib/api/leads.ts @@ -107,6 +107,10 @@ export const leadsApi = { return apiClient.post(`/leads/${id}/convert`, {}); }, + async revert(id: number): Promise { + return apiClient.post(`/leads/${id}/revert`, {}); + }, + async getTableViewLeads(filters?: { search?: string; status?: string;