From 50ec248178829d20ce69f416bed1a31305db863d Mon Sep 17 00:00:00 2001 From: ashwin Date: Sat, 24 May 2025 17:38:49 +0530 Subject: [PATCH 1/5] Refactor lead detail page layout and enhance form handling - Updated the layout to improve responsiveness and visual hierarchy. - Removed unused imports and optimized component usage. - Enhanced form handling for converting leads and adding comments. - Improved toast notifications for better user feedback. - Added new sections for lead information and additional details. - Updated the comments section to display user avatars and timestamps. - Improved accessibility and semantic structure of the HTML. --- package.json | 4 +- pnpm-lock.yaml | 20 + .../(app)/app/leads/[lead_id]/+page.server.js | 250 ++++----- .../(app)/app/leads/[lead_id]/+page.svelte | 519 +++++++----------- 4 files changed, 325 insertions(+), 468 deletions(-) diff --git a/package.json b/package.json index f594e66..789629e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "dependencies": { "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@lucide/svelte": "^0.511.0", "@prisma/client": "6.5.0", "axios": "^1.8.4", "date-fns": "^4.1.0", @@ -53,6 +54,7 @@ "flowbite-svelte-icons": "^2.1.1", "marked": "^15.0.8", "svelte-fa": "^4.0.3", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "zod": "^3.25.28" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 140ae3a..31534cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fortawesome/free-solid-svg-icons': specifier: ^6.7.2 version: 6.7.2 + '@lucide/svelte': + specifier: ^0.511.0 + version: 0.511.0(svelte@5.27.0) '@prisma/client': specifier: 6.5.0 version: 6.5.0(prisma@6.5.0(typescript@5.8.3))(typescript@5.8.3) @@ -38,6 +41,9 @@ importers: uuid: specifier: ^11.1.0 version: 11.1.0 + zod: + specifier: ^3.25.28 + version: 3.25.28 devDependencies: '@eslint/compat': specifier: ^1.2.5 @@ -375,6 +381,11 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lucide/svelte@0.511.0': + resolution: {integrity: sha512-aLCSPMUJmHlCuLXzXENXa4Z1NV2mN1iAZAFKk4bEbey+/MdsNlu+/DqwVkgW3Yvj6p8y8Vn5xZ2v9CLmPlA6Vw==} + peerDependencies: + svelte: ^5 + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1635,6 +1646,9 @@ packages: zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod@3.25.28: + resolution: {integrity: sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==} + snapshots: '@ampproject/remapping@2.3.0': @@ -1820,6 +1834,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@lucide/svelte@0.511.0(svelte@5.27.0)': + dependencies: + svelte: 5.27.0 + '@polka/url@1.0.0-next.29': {} '@popperjs/core@2.11.8': {} @@ -2935,3 +2953,5 @@ snapshots: yocto-queue@0.1.0: {} zimmerframe@1.1.2: {} + + zod@3.25.28: {} diff --git a/src/routes/(app)/app/leads/[lead_id]/+page.server.js b/src/routes/(app)/app/leads/[lead_id]/+page.server.js index 4eeca70..c7b01ed 100644 --- a/src/routes/(app)/app/leads/[lead_id]/+page.server.js +++ b/src/routes/(app)/app/leads/[lead_id]/+page.server.js @@ -1,77 +1,71 @@ import { error, fail } from '@sveltejs/kit'; import { PrismaClient } from '@prisma/client'; +import { z } from 'zod'; // For input validation const prisma = new PrismaClient(); -export async function load({ params }) { +// Input validation schemas +const commentSchema = z.object({ + comment: z.string().min(1, 'Comment cannot be empty').max(1000, 'Comment too long').trim() +}); + +export async function load({ params, locals }) { const lead_id = params.lead_id; - - try { - // Fetch lead with owner information - const lead = await prisma.lead.findUnique({ - where: { id: lead_id }, - include: { - owner: true, - tasks: { - orderBy: { createdAt: 'desc' } - }, - events: { - orderBy: { startDate: 'asc' } - }, - comments: { - include: { - author: true - }, - orderBy: { createdAt: 'desc' } + const org = locals.org; + + const lead = await prisma.lead.findUnique({ + where: { id: lead_id, organizationId: org.id }, + include: { + owner: true, + tasks: { + orderBy: { createdAt: 'desc' } + }, + events: { + orderBy: { startDate: 'asc' } + }, + comments: { + include: { + author: true }, - contact: true - } - }); - console.log('Lead:', lead); - if (!lead) { - throw error(404, 'Lead not found'); + orderBy: { createdAt: 'desc' } + }, + contact: true } - - return { - lead - }; - } catch (err) { - console.error('Error fetching lead:', err); - throw error(500, 'Failed to load lead details'); + }); + + if (!lead) { + throw error(404, 'Lead not found'); } + + console.log('Loaded lead:', lead); + return { + lead + }; } -// Add form actions export const actions = { - // Action to convert a lead to contact/account/opportunity - convert: async ({ params }) => { + convert: async ({ params, locals }) => { const lead_id = params.lead_id; - + const user = locals.user; + const org = locals.org; + try { - // Get the lead to convert with organization data const lead = await prisma.lead.findUnique({ - where: { id: lead_id }, + where: { id: lead_id, organizationId: org.id }, include: { organization: true, owner: true } }); - + if (!lead) { - return fail(404, { - status: 'error', - message: 'Lead not found' - }); + return fail(404, { status: 'error', message: 'Lead not found' }); } - + if (lead.status === 'CONVERTED') { - return { - status: 'success', - message: 'Lead already converted' - }; + return { status: 'success', message: 'Lead already converted' }; } - - // Create a new contact from the lead + const contact = await prisma.contact.create({ data: { firstName: lead.firstName, @@ -80,17 +74,11 @@ export const actions = { phone: lead.phone, title: lead.title, description: lead.description, - // Connect to the required relationships - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } } }); - - // Create a new account if company info exists + let accountId = null; let account = null; if (lead.company) { @@ -98,96 +86,60 @@ export const actions = { data: { name: lead.company, industry: lead.industry, - // Connect to the required relationships - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } } }); accountId = account.id; - - // Create the relationship between account and contact using AccountContactRelationship model + await prisma.accountContactRelationship.create({ data: { - account: { - connect: { id: account.id } - }, - contact: { - connect: { id: contact.id } - }, + account: { connect: { id: account.id } }, + contact: { connect: { id: contact.id } }, isPrimary: true, role: 'Primary Contact' } }); } - - // Create an opportunity - // First build the common data for the opportunity + const opportunityData = { name: `${lead.company || lead.firstName + ' ' + lead.lastName} Opportunity`, stage: 'PROSPECTING', amount: 0, - closeDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now - - // Connect to the required relationships - contacts: { - connect: { id: contact.id } - }, - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + closeDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + contacts: { connect: { id: contact.id } }, + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } }; - - // According to the schema, an opportunity must be connected to an account - // If no account was created, we'll need to create a placeholder account + if (!accountId) { const placeholderAccount = await prisma.account.create({ data: { name: `${lead.firstName} ${lead.lastName} Account`, - // Connect to the required relationships - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } } }); - + accountId = placeholderAccount.id; account = placeholderAccount; - - // Create relationship between contact and placeholder account + await prisma.accountContactRelationship.create({ data: { - account: { - connect: { id: placeholderAccount.id } - }, - contact: { - connect: { id: contact.id } - }, + account: { connect: { id: placeholderAccount.id } }, + contact: { connect: { id: contact.id } }, isPrimary: true, role: 'Primary Contact' } }); } - - // Now add the account to the opportunity data - opportunityData.account = { - connect: { id: accountId } - }; - + + opportunityData.account = { connect: { id: accountId } }; + const opportunity = await prisma.opportunity.create({ data: opportunityData }); - - // Update the lead as converted + await prisma.lead.update({ where: { id: lead_id }, data: { @@ -197,12 +149,10 @@ export const actions = { convertedContactId: contact.id, convertedAccountId: accountId, convertedOpportunityId: opportunity.id, - contact: { - connect: { id: contact.id } - } + contact: { connect: { id: contact.id } } } }); - + return { status: 'success', message: 'Lead successfully converted', @@ -211,72 +161,60 @@ export const actions = { opportunity }; } catch (err) { - console.error('Error converting lead:', err); - return fail(500, { - status: 'error', - message: 'Failed to convert lead: ' + (err.message || 'Unknown error') - }); + console.error('Error converting lead:', err.message); + return fail(500, { status: 'error', message: 'Failed to convert lead' }); } }, - - // Action to add a comment to the lead + addComment: async ({ params, request, locals }) => { const lead_id = params.lead_id; + const user = locals.user; + const org = locals.org; + + + // Validate form data const data = await request.formData(); const comment = data.get('comment'); - const commentValue = typeof comment === 'string' ? comment : String(comment); - if (!commentValue.trim()) { - return fail(400, { - status: 'error', - message: 'Comment cannot be empty' - }); - } + try { - // Use the logged-in user from locals - const user = locals.user; - if (!user) { - return fail(401, { - status: 'error', - message: 'You must be logged in to comment.' - }); - } - // Get the lead to obtain its organization ID + const validatedComment = commentSchema.parse({ comment }); + const lead = await prisma.lead.findUnique({ - where: { id: lead_id }, + where: { id: lead_id, organizationId: org.id }, select: { organizationId: true } }); + if (!lead) { - return fail(404, { - status: 'error', - message: 'Lead not found' - }); + return fail(404, { status: 'error', message: 'Lead not found' }); } + await prisma.comment.create({ data: { - body: commentValue, + body: validatedComment.comment, lead: { connect: { id: lead_id } }, author: { connect: { id: user.id } }, organization: { connect: { id: lead.organizationId } } } }); - // Refetch comments for immediate update + const updatedLead = await prisma.lead.findUnique({ where: { id: lead_id }, include: { comments: { include: { author: true }, orderBy: { createdAt: 'desc' } } } }); + return { status: 'success', message: 'Comment added successfully', comments: updatedLead?.comments || [] }; } catch (err) { - console.error('Error adding comment:', err); - return fail(500, { - status: 'error', - message: 'Failed to add comment' - }); + console.error('Error adding comment:', err.message); + if (err instanceof z.ZodError) { + return fail(400, { status: 'error', message: err.errors[0].message }); + } + return fail(500, { status: 'error', message: 'Failed to add comment' }); } } -}; +}; \ No newline at end of file diff --git a/src/routes/(app)/app/leads/[lead_id]/+page.svelte b/src/routes/(app)/app/leads/[lead_id]/+page.svelte index 6740673..d124e8a 100644 --- a/src/routes/(app)/app/leads/[lead_id]/+page.svelte +++ b/src/routes/(app)/app/leads/[lead_id]/+page.svelte @@ -1,9 +1,9 @@ - {#if showToast} - + {toastMessage} {/if} -
- -
-
-
- - - - - -

{getFullName(lead)}

- {lead.status} -
-
- {#if lead.status !== 'CONVERTED'} - -
{ - isConverting = true; // Set loading state on submit - return async ({ update }) => { - // This runs after the action completes - await update({ reset: false }); // Update form prop without resetting the page - // isConverting will be reset by the reactive statement above - }; - }}> - -
- {/if} - -
-
-
+
+
+
+ +
+ + -
- -
- -
- - -
-
- - - -
-
-

{getFullName(lead)}

-
- {lead.status} -
- {#if lead.title} -
{lead.title}
- {/if} - {#if lead.company} -
{lead.company}
- {/if} + +
+
+ +
+
+

{getFullName(lead)}

+
+

Lead

+ {lead.status}
- - -
-

Contact Information

- {#if lead.email} -
-
- - - -
- {lead.email} -
- {/if} - {#if lead.phone} -
-
- - - -
- {lead.phone} -
+
+ {#if lead.status !== 'CONVERTED'} +
+ +
{/if} +
+
- -
-

Lead Details

-
- {#if lead.leadSource} -
- Source - {lead.leadSource.replace('_', ' ').toLowerCase()} -
- {/if} - {#if lead.industry} -
- Industry - {lead.industry} -
- {/if} - {#if lead.rating} -
- Rating -
- {#each Array(parseInt(lead.rating) || 0) as _, i} - - - + +
+ +
+
+

Lead Information

+
+ {#each [ + { label: 'Full Name', value: getFullName(lead) }, + { label: 'Company', value: lead.company }, + { label: 'Email', value: lead.email, href: `mailto:${lead.email}` }, + { label: 'Phone', value: lead.phone, href: `tel:${lead.phone}` }, + { label: 'Lead Source', value: lead.leadSource?.replace('_', ' ')?.toLowerCase(), capitalize: true }, + { label: 'Industry', value: lead.industry } + ] as item} + {#if item.value} +
+

{item.label}

+ {#if item.href} +

{item.value}

+ {:else} +

{item.value}

+ {/if} +
+ {/if} {/each}
-
- {/if} -
-
+ - -
-

Timeline

-
-
-
-
- Created - {formatDate(lead.createdAt)} -
+
+

Additional Details

+
+ {#if lead.rating} +
+

Rating

+ + {#each Array(parseInt(lead.rating) || 0) as _, i} + + {/each} + {#each Array(5 - (parseInt(lead.rating) || 0)) as _, i} + + {/each} + +
+ {/if} + {#if lead.annualRevenue} +
+

Annual Revenue

+

${lead.annualRevenue.toLocaleString()}

+
+ {/if} + {#if lead.address} +
+

Address

+

{lead.address}

+
+ {/if} +
+
+ +
+
+
+

Status

+

{lead.status}

+
+
+

Lead Owner

+

{lead.owner?.name || 'Unassigned'}

+
+
+

Created At

+

{formatDate(lead.createdAt)}

+
+
+

Updated At

+

{formatDate(lead.updatedAt)}

+
+ {#if lead.isConverted && lead.convertedAt} +
+

Converted At

+

{formatDate(lead.convertedAt)}

+
+ {/if} +
+
-
-
-
- Updated - {formatDate(lead.updatedAt)} -
+
+ {@html lead.description}
- {#if lead.isConverted && lead.convertedAt} -
-
-
- Converted - {formatDate(lead.convertedAt)} -
-
- {/if} -
-
- +
+
- - -

Lead Owner

-
-
- - - -
-
-
{lead.owner?.name || 'Unassigned'}
- {#if lead.owner?.email} -
{lead.owner.email}
- {/if} + +