Skip to content

Commit 68bb50e

Browse files
committed
feat: implement opportunity deletion functionality with confirmation modal
1 parent 9d3f8ba commit 68bb50e

File tree

4 files changed

+372
-8
lines changed

4 files changed

+372
-8
lines changed

src/routes/(app)/app/opportunities/+page.server.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PrismaClient } from '@prisma/client';
2+
import { fail } from '@sveltejs/kit';
23

34
const prisma = new PrismaClient();
45

@@ -119,4 +120,44 @@ export async function load({ locals }) {
119120
}
120121
};
121122
}
123+
};
124+
125+
/** @type {import('./$types').Actions} */
126+
export const actions = {
127+
delete: async ({ request, locals }) => {
128+
try {
129+
const formData = await request.formData();
130+
const opportunityId = formData.get('opportunityId')?.toString();
131+
const userId = locals.user?.id;
132+
const organizationId = locals.org?.id;
133+
134+
if (!opportunityId || !userId || !organizationId) {
135+
return fail(400, { message: 'Missing required data' });
136+
}
137+
138+
// Check if the opportunity exists and belongs to the user's organization
139+
const opportunity = await prisma.opportunity.findFirst({
140+
where: {
141+
id: opportunityId,
142+
organizationId: organizationId
143+
}
144+
});
145+
146+
if (!opportunity) {
147+
return fail(404, { message: 'Opportunity not found' });
148+
}
149+
150+
// Delete the opportunity
151+
await prisma.opportunity.delete({
152+
where: {
153+
id: opportunityId
154+
}
155+
});
156+
157+
return { success: true, message: 'Opportunity deleted successfully' };
158+
} catch (error) {
159+
console.error('Error deleting opportunity:', error);
160+
return fail(500, { message: 'Failed to delete opportunity' });
161+
}
162+
}
122163
};

src/routes/(app)/app/opportunities/+page.svelte

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,25 @@
1717
CheckCircle,
1818
XCircle,
1919
Clock,
20-
Target
20+
Target,
21+
X,
22+
AlertTriangle
2123
} from '@lucide/svelte';
2224
import { goto } from '$app/navigation';
25+
import { enhance } from '$app/forms';
26+
import { page } from '$app/stores';
2327
24-
/** @type {{ data: import('./$types').PageData }} */
25-
let { data } = $props();
28+
/** @type {{ data: import('./$types').PageData, form?: any }} */
29+
let { data, form } = $props();
2630
2731
let searchTerm = $state('');
2832
let selectedStage = $state('all');
2933
let sortField = $state('createdAt');
3034
let sortDirection = $state('desc');
3135
let showFilters = $state(false);
36+
let showDeleteModal = $state(false);
37+
let opportunityToDelete = $state(null);
38+
let deleteLoading = $state(false);
3239
3340
// Stage configurations
3441
const stageConfig = {
@@ -101,13 +108,43 @@
101108
sortDirection = 'asc';
102109
}
103110
}
111+
112+
function openDeleteModal(opportunity) {
113+
opportunityToDelete = opportunity;
114+
showDeleteModal = true;
115+
}
116+
117+
function closeDeleteModal() {
118+
showDeleteModal = false;
119+
opportunityToDelete = null;
120+
deleteLoading = false;
121+
}
104122
</script>
105123
106124
<svelte:head>
107125
<title>Opportunities - BottleCRM</title>
108126
</svelte:head>
109127
110128
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
129+
<!-- Success/Error Messages -->
130+
{#if form?.success}
131+
<div class="fixed top-4 right-4 z-50 max-w-md">
132+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative" role="alert">
133+
<strong class="font-bold">Success!</strong>
134+
<span class="block sm:inline">{form.message || 'Opportunity deleted successfully.'}</span>
135+
</div>
136+
</div>
137+
{/if}
138+
139+
{#if form?.message && !form?.success}
140+
<div class="fixed top-4 right-4 z-50 max-w-md">
141+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
142+
<strong class="font-bold">Error!</strong>
143+
<span class="block sm:inline">{form.message}</span>
144+
</div>
145+
</div>
146+
{/if}
147+
111148
<!-- Header -->
112149
<div class="bg-white dark:bg-gray-800 shadow">
113150
<div class="px-4 sm:px-6 lg:px-8">
@@ -342,7 +379,10 @@
342379
</td>
343380
<td class="px-6 py-4 whitespace-nowrap">
344381
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {config.color}">
345-
<svelte:component this={config.icon} class="mr-1 h-3 w-3" />
382+
{#if config.icon}
383+
{@const IconComponent = config.icon}
384+
<IconComponent class="mr-1 h-3 w-3" />
385+
{/if}
346386
{config.label}
347387
</span>
348388
</td>
@@ -386,13 +426,13 @@
386426
</td>
387427
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
388428
<div class="flex items-center justify-end space-x-2">
389-
<button
390-
type="button"
429+
<a
430+
href="/app/opportunities/{opportunity.id}"
391431
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
392432
title="View"
393433
>
394434
<Eye class="h-4 w-4" />
395-
</button>
435+
</a>
396436
<a
397437
href="/app/opportunities/{opportunity.id}/edit"
398438
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300"
@@ -402,6 +442,7 @@
402442
</a>
403443
<button
404444
type="button"
445+
onclick={() => openDeleteModal(opportunity)}
405446
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
406447
title="Delete"
407448
>
@@ -439,4 +480,95 @@
439480
</div>
440481
441482
442-
</div>
483+
</div>
484+
485+
<!-- Delete Confirmation Modal -->
486+
{#if showDeleteModal && opportunityToDelete}
487+
<div
488+
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"
489+
role="dialog"
490+
aria-modal="true"
491+
aria-labelledby="modal-title"
492+
tabindex="-1"
493+
onclick={closeDeleteModal}
494+
onkeydown={(e) => e.key === 'Escape' && closeDeleteModal()}
495+
>
496+
<div
497+
class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800"
498+
role="button"
499+
tabindex="0"
500+
onkeydown={(e) => e.key === 'Escape' && closeDeleteModal()}
501+
onclick={(e) => e.stopPropagation()}
502+
>
503+
<div class="mt-3">
504+
<div class="flex items-center justify-between mb-4">
505+
<div class="flex items-center">
506+
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900">
507+
<AlertTriangle class="h-6 w-6 text-red-600 dark:text-red-400" />
508+
</div>
509+
<div class="ml-4">
510+
<h3 id="modal-title" class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Delete Opportunity</h3>
511+
</div>
512+
</div>
513+
<button
514+
type="button"
515+
onclick={closeDeleteModal}
516+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
517+
>
518+
<X class="h-5 w-5" />
519+
</button>
520+
</div>
521+
522+
<div class="mt-2">
523+
<p class="text-sm text-gray-500 dark:text-gray-400">
524+
Are you sure you want to delete the opportunity <strong>"{opportunityToDelete?.name || 'Unknown'}"</strong>?
525+
This action cannot be undone and will also delete all associated tasks, events, and comments.
526+
</p>
527+
</div>
528+
529+
<div class="mt-6 flex justify-end space-x-3">
530+
<button
531+
type="button"
532+
onclick={closeDeleteModal}
533+
disabled={deleteLoading}
534+
class="px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
535+
>
536+
Cancel
537+
</button>
538+
539+
<form method="POST" action="?/delete" use:enhance={({ formElement, formData }) => {
540+
deleteLoading = true;
541+
542+
return async ({ result }) => {
543+
deleteLoading = false;
544+
545+
if (result.type === 'success') {
546+
closeDeleteModal();
547+
// Use goto with replaceState and invalidateAll for a clean refresh
548+
await goto($page.url.pathname, {
549+
replaceState: true,
550+
invalidateAll: true
551+
});
552+
} else if (result.type === 'failure') {
553+
console.error('Delete failed:', result.data?.message);
554+
alert('Failed to delete opportunity: ' + (result.data?.message || 'Unknown error'));
555+
} else if (result.type === 'error') {
556+
console.error('Delete error:', result.error);
557+
alert('An error occurred while deleting the opportunity.');
558+
}
559+
};
560+
}}>
561+
<input type="hidden" name="opportunityId" value={opportunityToDelete?.id || ''} />
562+
<button
563+
type="submit"
564+
disabled={deleteLoading}
565+
class="px-4 py-2 bg-red-600 border border-transparent rounded-md text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
566+
>
567+
{deleteLoading ? 'Deleting...' : 'Delete'}
568+
</button>
569+
</form>
570+
</div>
571+
</div>
572+
</div>
573+
</div>
574+
{/if}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { error, redirect } from '@sveltejs/kit';
2+
import { fail } from '@sveltejs/kit';
3+
import prisma from '$lib/prisma';
4+
5+
export async function load({ params, locals }) {
6+
const userId = locals.user?.id;
7+
const organizationId = locals.org?.id;
8+
9+
if (!userId || !organizationId) {
10+
throw error(401, 'Unauthorized');
11+
}
12+
13+
const opportunity = await prisma.opportunity.findFirst({
14+
where: {
15+
id: params.opportunityId,
16+
organizationId: organizationId
17+
},
18+
include: {
19+
account: {
20+
select: {
21+
id: true,
22+
name: true
23+
}
24+
},
25+
owner: {
26+
select: {
27+
id: true,
28+
name: true,
29+
email: true
30+
}
31+
}
32+
}
33+
});
34+
35+
if (!opportunity) {
36+
throw error(404, 'Opportunity not found');
37+
}
38+
39+
return {
40+
opportunity
41+
};
42+
}
43+
44+
/** @type {import('./$types').Actions} */
45+
export const actions = {
46+
default: async ({ params, locals }) => {
47+
try {
48+
const userId = locals.user?.id;
49+
const organizationId = locals.org?.id;
50+
51+
if (!userId || !organizationId) {
52+
return fail(401, { message: 'Unauthorized' });
53+
}
54+
55+
// Check if the opportunity exists and belongs to the user's organization
56+
const opportunity = await prisma.opportunity.findFirst({
57+
where: {
58+
id: params.opportunityId,
59+
organizationId: organizationId
60+
}
61+
});
62+
63+
if (!opportunity) {
64+
return fail(404, { message: 'Opportunity not found' });
65+
}
66+
67+
// Delete the opportunity (this will cascade delete related records)
68+
await prisma.opportunity.delete({
69+
where: {
70+
id: params.opportunityId
71+
}
72+
});
73+
74+
// Redirect to opportunities list
75+
throw redirect(303, '/app/opportunities');
76+
} catch (err) {
77+
if (err instanceof Response && err.status === 303) {
78+
throw err; // Re-throw redirect
79+
}
80+
console.error('Error deleting opportunity:', err);
81+
return fail(500, { message: 'Failed to delete opportunity' });
82+
}
83+
}
84+
};

0 commit comments

Comments
 (0)