From d271fa3bd86de0bde4df9dda824933af3dd91215 Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 18 Jun 2025 08:00:03 +0530 Subject: [PATCH 01/11] feat: improve form styling and accessibility for organization and user management --- src/routes/(app)/app/users/+page.svelte | 66 +++++++++++++------------ 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/routes/(app)/app/users/+page.svelte b/src/routes/(app)/app/users/+page.svelte index 47f0b0f..c2a22bb 100644 --- a/src/routes/(app)/app/users/+page.svelte +++ b/src/routes/(app)/app/users/+page.svelte @@ -128,58 +128,59 @@
-
@@ -428,7 +428,7 @@ name="postalCode" type="text" bind:value={formData.postalCode} - on:input={handleChange} + oninput={handleChange} placeholder="Postal code" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" />
@@ -472,7 +472,7 @@ type="number" min="0" bind:value={formData.numberOfEmployees} - on:input={handleChange} + oninput={handleChange} placeholder="100" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.numberOfEmployees ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> {#if errors.numberOfEmployees} @@ -493,7 +493,7 @@ min="0" step="0.01" bind:value={formData.annualRevenue} - on:input={handleChange} + oninput={handleChange} placeholder="1000000" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.annualRevenue ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> {#if errors.annualRevenue} @@ -512,7 +512,7 @@ name="tickerSymbol" type="text" bind:value={formData.tickerSymbol} - on:input={handleChange} + oninput={handleChange} placeholder="AAPL" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -527,7 +527,7 @@ name="sicCode" type="text" bind:value={formData.sicCode} - on:input={handleChange} + oninput={handleChange} placeholder="7372" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -550,7 +550,7 @@ id="description" name="description" bind:value={formData.description} - on:input={handleChange} + oninput={handleChange} placeholder="Additional notes about this account..." rows="4" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors resize-vertical"> diff --git a/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js b/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js index 83a01b6..dd0b513 100644 --- a/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js +++ b/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js @@ -1,5 +1,6 @@ import prisma from '$lib/prisma'; import { fail, redirect } from '@sveltejs/kit'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; export async function load({ params, locals }) { const org = locals.org; @@ -47,6 +48,16 @@ export const actions = { return fail(400, { message: 'First and last name are required.' }); } + // Validate phone number if provided + let formattedPhone = null; + if (phone && phone.length > 0) { + const phoneValidation = validatePhoneNumber(phone); + if (!phoneValidation.isValid) { + return fail(400, { message: phoneValidation.error || 'Please enter a valid phone number' }); + } + formattedPhone = formatPhoneForStorage(phone); + } + const contact = await prisma.contact.findUnique({ where: { id: params.contactId, organizationId: org.id } }); @@ -61,7 +72,7 @@ export const actions = { firstName, lastName, email, - phone, + phone: formattedPhone, title, department, street, diff --git a/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte b/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte index 25e5d38..80a3f45 100644 --- a/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte +++ b/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte @@ -4,6 +4,8 @@ import { onMount } from 'svelte'; import { invalidateAll } from '$app/navigation'; import { User, Mail, Phone, Building, MapPin, FileText, Star, Save, X, ArrowLeft } from '@lucide/svelte'; + import { validatePhoneNumber } from '$lib/utils/phone.js'; + export let data; let contact = data.contact; @@ -25,6 +27,22 @@ let description = contact.description || ''; let submitting = false; let errorMsg = ''; + let phoneError = ''; + + // Validate phone number on input + function validatePhone() { + if (!phone.trim()) { + phoneError = ''; + return; + } + + const validation = validatePhoneNumber(phone); + if (!validation.isValid) { + phoneError = validation.error || 'Invalid phone number'; + } else { + phoneError = ''; + } + } async function handleSubmit(e) { e.preventDefault(); @@ -185,8 +203,12 @@ class="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" bind:value={phone} placeholder="+1 (555) 123-4567" + oninput={validatePhone} /> + {#if phoneError} +

{phoneError}

+ {/if} diff --git a/src/routes/(app)/app/contacts/new/+page.server.js b/src/routes/(app)/app/contacts/new/+page.server.js index 07bb5bb..e46de9b 100644 --- a/src/routes/(app)/app/contacts/new/+page.server.js +++ b/src/routes/(app)/app/contacts/new/+page.server.js @@ -1,5 +1,6 @@ import { redirect, fail } from '@sveltejs/kit'; import prisma from '$lib/prisma'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; /** @type {import('./$types').PageServerLoad} */ export async function load({ locals, url }) { @@ -94,6 +95,17 @@ export const actions = { errors.email = 'Please enter a valid email address'; } + // Validate phone number if provided + let formattedPhone = null; + if (phone && phone.length > 0) { + const phoneValidation = validatePhoneNumber(phone); + if (!phoneValidation.isValid) { + errors.phone = phoneValidation.error || 'Please enter a valid phone number'; + } else { + formattedPhone = formatPhoneForStorage(phone); + } + } + if (Object.keys(errors).length > 0) { return fail(400, { errors, @@ -199,7 +211,7 @@ export const actions = { firstName, lastName, email: email || null, - phone: phone || null, + phone: formattedPhone, title: title || null, department: department || null, street: street || null, diff --git a/src/routes/(app)/app/contacts/new/+page.svelte b/src/routes/(app)/app/contacts/new/+page.svelte index 85ed39a..dac84da 100644 --- a/src/routes/(app)/app/contacts/new/+page.svelte +++ b/src/routes/(app)/app/contacts/new/+page.svelte @@ -2,11 +2,13 @@ import { enhance } from '$app/forms'; import { page } from '$app/stores'; import { ArrowLeft, User, Mail, Phone, Building, MapPin, FileText, Save } from '@lucide/svelte'; + import { validatePhoneNumber } from '$lib/utils/phone.js'; /** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */ let { data, form } = $props(); let isSubmitting = $state(false); + let phoneError = $state(''); // Get accountId from URL parameters const accountId = $page.url.searchParams.get('accountId'); @@ -45,6 +47,21 @@ isSubmitting = false; }; } + + // Validate phone number on input + function validatePhone() { + if (!formValues.phone.trim()) { + phoneError = ''; + return; + } + + const validation = validatePhoneNumber(formValues.phone); + if (!validation.isValid) { + phoneError = validation.error || 'Invalid phone number'; + } else { + phoneError = ''; + } + } @@ -222,9 +239,16 @@ id="phone" name="phone" bind:value={formValues.phone} - class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + oninput={validatePhone} + class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 {errors.phone ? 'border-red-300 dark:border-red-600' : ''}" placeholder="+1 (555) 123-4567" /> + {#if errors.phone} +

{errors.phone}

+ {/if} + {#if phoneError} +

{phoneError}

+ {/if} diff --git a/src/routes/(app)/app/leads/new/+page.server.js b/src/routes/(app)/app/leads/new/+page.server.js index 47943e3..6c379c0 100644 --- a/src/routes/(app)/app/leads/new/+page.server.js +++ b/src/routes/(app)/app/leads/new/+page.server.js @@ -2,6 +2,7 @@ import { env } from '$env/dynamic/private'; import { redirect } from '@sveltejs/kit'; import prisma from '$lib/prisma'; import { fail } from '@sveltejs/kit'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; import { industries, leadSources, @@ -54,13 +55,24 @@ export const actions = { return fail(400, { error: 'Lead title is required' }); } + // Validate phone number if provided + let formattedPhone = null; + const phone = formData.get('phone')?.toString(); + if (phone && phone.trim().length > 0) { + const phoneValidation = validatePhoneNumber(phone.trim()); + if (!phoneValidation.isValid) { + return fail(400, { error: phoneValidation.error || 'Please enter a valid phone number' }); + } + formattedPhone = formatPhoneForStorage(phone.trim()); + } + // Extract all form fields const leadData = { firstName, lastName, title: leadTitle, email: email || null, - phone: formData.get('phone')?.toString() || null, + phone: formattedPhone, company: formData.get('company')?.toString() || null, status: (formData.get('status')?.toString() || 'PENDING'), leadSource: formData.get('source')?.toString() || null, diff --git a/src/routes/(app)/app/leads/new/+page.svelte b/src/routes/(app)/app/leads/new/+page.svelte index 1ef9f44..da5923a 100644 --- a/src/routes/(app)/app/leads/new/+page.svelte +++ b/src/routes/(app)/app/leads/new/+page.svelte @@ -2,6 +2,7 @@ import { enhance } from '$app/forms'; import { goto } from '$app/navigation'; import { fade } from 'svelte/transition'; + import { validatePhoneNumber } from '$lib/utils/phone.js'; // Lucide icons import { @@ -30,7 +31,8 @@ let showToast = false; let toastMessage = ''; let toastType = 'success'; // 'success' | 'error' - + let phoneError = ''; + /** * Object holding the form fields. */ @@ -116,9 +118,12 @@ } // Validate phone format if provided - if (formData.phone && !/^[\d\s\-+()]*$/.test(formData.phone)) { - errors.phone = 'Please enter a valid phone number'; - isValid = false; + if (formData.phone && formData.phone.trim().length > 0) { + const phoneValidation = validatePhoneNumber(formData.phone); + if (!phoneValidation.isValid) { + errors.phone = phoneValidation.error || 'Please enter a valid phone number'; + isValid = false; + } } // Validate website URL if provided @@ -191,6 +196,23 @@ showToast = true; setTimeout(() => showToast = false, 5000); } + + /** + * Validates phone number on input + */ + function validatePhone() { + if (!formData.phone.trim()) { + phoneError = ''; + return; + } + + const validation = validatePhoneNumber(formData.phone); + if (!validation.isValid) { + phoneError = validation.error || 'Invalid phone number'; + } else { + phoneError = ''; + } + } @@ -287,7 +309,7 @@ name="lead_title" type="text" bind:value={formData.lead_title} - on:input={handleChange} + oninput={handleChange} placeholder="Enter lead title" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.lead_title ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> @@ -307,7 +329,7 @@ name="company" type="text" bind:value={formData.company} - on:input={handleChange} + oninput={handleChange} placeholder="Company name" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -394,7 +416,7 @@ name="website" type="url" bind:value={formData.website} - on:input={handleChange} + oninput={handleChange} placeholder="https://company.com" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.website ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> {#if errors.website} @@ -413,7 +435,7 @@ id="opportunity_amount" name="opportunity_amount" bind:value={formData.opportunity_amount} - on:input={handleChange} + oninput={handleChange} placeholder="0" min="0" step="0.01" @@ -433,7 +455,7 @@ min="0" max="100" bind:value={formData.probability} - on:input={handleChange} + oninput={handleChange} placeholder="50" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.probability ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> {#if errors.probability} @@ -504,7 +526,7 @@ name="first_name" type="text" bind:value={formData.first_name} - on:input={handleChange} + oninput={handleChange} placeholder="First name" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.first_name ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> @@ -523,7 +545,7 @@ name="last_name" type="text" bind:value={formData.last_name} - on:input={handleChange} + oninput={handleChange} placeholder="Last name" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.last_name ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> @@ -542,7 +564,7 @@ name="title" type="text" bind:value={formData.title} - on:input={handleChange} + oninput={handleChange} placeholder="Job title" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -558,12 +580,16 @@ name="phone" type="tel" bind:value={formData.phone} - on:input={handleChange} + oninput={handleChange} + on:blur={validatePhone} placeholder="+1 (555) 123-4567" - class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.phone ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> + class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.phone || phoneError ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> {#if errors.phone}

{errors.phone}

{/if} + {#if phoneError} +

{phoneError}

+ {/if} @@ -577,7 +603,7 @@ type="email" name="email" bind:value={formData.email} - on:input={handleChange} + oninput={handleChange} placeholder="email@company.com" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.email ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> @@ -596,7 +622,7 @@ name="linkedin_url" type="url" bind:value={formData.linkedin_url} - on:input={handleChange} + oninput={handleChange} placeholder="https://linkedin.com/in/username" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors {errors.linkedin_url ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" /> {#if errors.linkedin_url} @@ -627,7 +653,7 @@ name="address_line" type="text" bind:value={formData.address_line} - on:input={handleChange} + oninput={handleChange} placeholder="Street address" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -639,7 +665,7 @@ name="city" type="text" bind:value={formData.city} - on:input={handleChange} + oninput={handleChange} placeholder="City" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -651,7 +677,7 @@ name="state" type="text" bind:value={formData.state} - on:input={handleChange} + oninput={handleChange} placeholder="State" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -663,7 +689,7 @@ name="postcode" type="text" bind:value={formData.postcode} - on:input={handleChange} + oninput={handleChange} placeholder="Postal code" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -675,7 +701,7 @@ name="country" type="text" bind:value={formData.country} - on:input={handleChange} + oninput={handleChange} placeholder="Country" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -698,7 +724,7 @@ id="description" name="description" bind:value={formData.description} - on:input={handleChange} + oninput={handleChange} placeholder="Additional notes about this lead..." rows="3" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors resize-vertical"> @@ -713,7 +739,7 @@ id="pain_points" name="pain_points" bind:value={formData.pain_points} - on:input={handleChange} + oninput={handleChange} placeholder="What challenges is the lead facing?" rows="3" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors resize-vertical"> @@ -730,7 +756,7 @@ name="last_contacted" type="date" bind:value={formData.last_contacted} - on:input={handleChange} + oninput={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> @@ -743,7 +769,7 @@ name="next_follow_up" type="date" bind:value={formData.next_follow_up} - on:input={handleChange} + oninput={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" /> diff --git a/src/routes/(app)/app/profile/+page.server.js b/src/routes/(app)/app/profile/+page.server.js index 48f084d..67a0bf2 100644 --- a/src/routes/(app)/app/profile/+page.server.js +++ b/src/routes/(app)/app/profile/+page.server.js @@ -1,5 +1,6 @@ import prisma from '$lib/prisma'; import { fail, redirect } from '@sveltejs/kit'; +import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js'; /** @type {import('./$types').PageServerLoad} */ export async function load({ locals }) { @@ -68,14 +69,16 @@ export const actions = { } // Validate phone if provided + let formattedPhone = null; if (phone && phone.trim().length > 0) { - const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/; - if (!phoneRegex.test(phone.replace(/[\s\-\(\)]/g, ''))) { + const phoneValidation = validatePhoneNumber(phone.trim()); + if (!phoneValidation.isValid) { return fail(400, { - error: 'Please enter a valid phone number', + error: phoneValidation.error || 'Please enter a valid phone number', data: { name, phone } }); } + formattedPhone = formatPhoneForStorage(phone.trim()); } try { @@ -85,7 +88,7 @@ export const actions = { }, data: { name: name.trim(), - phone: phone?.trim() || null, + phone: formattedPhone, updatedAt: new Date() } }); diff --git a/src/routes/(app)/app/profile/+page.svelte b/src/routes/(app)/app/profile/+page.svelte index a88e0bc..5cf3ffd 100644 --- a/src/routes/(app)/app/profile/+page.svelte +++ b/src/routes/(app)/app/profile/+page.svelte @@ -1,12 +1,14 @@ @@ -108,6 +126,25 @@
+ + {#if form?.success} +
+ +
+ {/if} + + {#if form?.message && !form?.success} +
+ +
+ {/if} +
@@ -342,7 +379,10 @@ - + {#if config.icon} + {@const IconComponent = config.icon} + + {/if} {config.label} @@ -386,13 +426,13 @@ -
\ No newline at end of file +
+ + +{#if showDeleteModal && opportunityToDelete} + +{/if} \ No newline at end of file diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.server.js b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.server.js new file mode 100644 index 0000000..b032ea3 --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.server.js @@ -0,0 +1,84 @@ +import { error, redirect } from '@sveltejs/kit'; +import { fail } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; + +export async function load({ params, locals }) { + const userId = locals.user?.id; + const organizationId = locals.org?.id; + + if (!userId || !organizationId) { + throw error(401, 'Unauthorized'); + } + + const opportunity = await prisma.opportunity.findFirst({ + where: { + id: params.opportunityId, + organizationId: organizationId + }, + include: { + account: { + select: { + id: true, + name: true + } + }, + owner: { + select: { + id: true, + name: true, + email: true + } + } + } + }); + + if (!opportunity) { + throw error(404, 'Opportunity not found'); + } + + return { + opportunity + }; +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ params, locals }) => { + try { + const userId = locals.user?.id; + const organizationId = locals.org?.id; + + if (!userId || !organizationId) { + return fail(401, { message: 'Unauthorized' }); + } + + // Check if the opportunity exists and belongs to the user's organization + const opportunity = await prisma.opportunity.findFirst({ + where: { + id: params.opportunityId, + organizationId: organizationId + } + }); + + if (!opportunity) { + return fail(404, { message: 'Opportunity not found' }); + } + + // Delete the opportunity (this will cascade delete related records) + await prisma.opportunity.delete({ + where: { + id: params.opportunityId + } + }); + + // Redirect to opportunities list + throw redirect(303, '/app/opportunities'); + } catch (err) { + if (err instanceof Response && err.status === 303) { + throw err; // Re-throw redirect + } + console.error('Error deleting opportunity:', err); + return fail(500, { message: 'Failed to delete opportunity' }); + } + } +}; diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.svelte b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.svelte new file mode 100644 index 0000000..3c750bb --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.svelte @@ -0,0 +1,107 @@ + + + + Delete Opportunity - BottleCRM + + +
+ +
+
+
+
+ + + +

Delete Opportunity

+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+

+ Confirm Deletion +

+

+ This action cannot be undone. +

+
+
+ +
+

+ You are about to delete: +

+
+
Opportunity: {data.opportunity.name}
+
Account: {data.opportunity.account?.name || 'N/A'}
+
Amount: {data.opportunity.amount ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(data.opportunity.amount) : 'N/A'}
+
Stage: {data.opportunity.stage}
+
+
+ +
+

+ Warning: Deleting this opportunity will also remove all associated: +

+
    +
  • Tasks and activities
  • +
  • Events and meetings
  • +
  • Comments and notes
  • +
  • Quote associations
  • +
+
+ +
+ + Cancel + + +
{ + deleteLoading = true; + return ({ result }) => { + deleteLoading = false; + if (result.type === 'success') { + goto('/app/opportunities'); + } + }; + }}> + +
+
+
+
+
+
+
From 5b617e20bdee0f899e151d932e34d92b0bb96ee3 Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 18 Jun 2025 10:34:20 +0530 Subject: [PATCH 06/11] Refactor event handlers from 'on:click' to 'onclick' for consistency across components - Updated all instances of 'on:click' to 'onclick' in various Svelte components to maintain a uniform coding style. - Adjusted the delete action in opportunities to return a success response instead of redirecting. - Enhanced error handling in the delete opportunity page to log errors and notify users appropriately. - Added navigation to opportunities list upon successful deletion of an opportunity. --- .../(app)/app/accounts/new/+page.svelte | 14 ++--- src/routes/(app)/app/cases/+page.svelte | 10 ++-- .../app/cases/[caseId]/edit/+page.svelte | 14 ++--- .../app/leads/[lead_id]/edit/+page.svelte | 4 +- src/routes/(app)/app/leads/new/+page.svelte | 16 ++--- .../[opportunityId]/delete/+page.server.js | 9 +-- .../[opportunityId]/delete/+page.svelte | 5 ++ .../app/tasks/[task_id]/edit/+page.svelte | 4 +- src/routes/(app)/app/users/+page.svelte | 8 +-- src/routes/(no-layout)/login/+page.svelte | 2 +- src/routes/(no-layout)/org/+page.svelte | 2 +- src/routes/(site)/+layout.svelte | 4 +- src/routes/(site)/+page.svelte | 12 ++-- src/routes/(site)/contact/+page.svelte | 14 ++--- src/routes/(site)/customization/+page.svelte | 12 ++-- src/routes/(site)/faq/+page.svelte | 60 +++++++++---------- .../features/contact-management/+page.svelte | 12 ++-- .../features/sales-pipeline/+page.svelte | 4 +- src/routes/(site)/migration/+page.svelte | 14 ++--- src/routes/(site)/pricing/+page.svelte | 12 ++-- 20 files changed, 117 insertions(+), 115 deletions(-) diff --git a/src/routes/(app)/app/accounts/new/+page.svelte b/src/routes/(app)/app/accounts/new/+page.svelte index 7c579c6..d624bec 100644 --- a/src/routes/(app)/app/accounts/new/+page.svelte +++ b/src/routes/(app)/app/accounts/new/+page.svelte @@ -164,7 +164,7 @@

{toastMessage}

@@ -259,7 +259,7 @@ id="type" name="type" bind:value={formData.type} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.accountTypes as [value, label]} @@ -276,7 +276,7 @@ id="industry" name="industry" bind:value={formData.industry} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.industries as [value, label]} @@ -293,7 +293,7 @@ id="rating" name="rating" bind:value={formData.rating} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.ratings as [value, label]} @@ -310,7 +310,7 @@ id="accountOwnership" name="accountOwnership" bind:value={formData.accountOwnership} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.accountOwnership as [value, label]} @@ -439,7 +439,7 @@ id="country" name="country" bind:value={formData.country} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.countries as [value, label]} @@ -564,7 +564,7 @@
@@ -343,7 +343,7 @@ id="source" name="source" bind:value={formData.source} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.source as [value, label]} @@ -361,7 +361,7 @@ id="industry" name="industry" bind:value={formData.industry} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.industries as [value, label]} @@ -379,7 +379,7 @@ id="status" name="status" bind:value={formData.status} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> {#each data.data.status as [value, label]} @@ -396,7 +396,7 @@ id="rating" name="rating" bind:value={formData.rating} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> @@ -472,7 +472,7 @@ id="budget_range" name="budget_range" bind:value={formData.budget_range} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> @@ -493,7 +493,7 @@ id="decision_timeframe" name="decision_timeframe" bind:value={formData.decision_timeframe} - on:change={handleChange} + onchange={handleChange} class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"> @@ -782,7 +782,7 @@
@@ -240,7 +240,7 @@ @@ -277,7 +277,7 @@ @@ -314,7 +314,7 @@ @@ -351,7 +351,7 @@ @@ -388,7 +388,7 @@ @@ -678,7 +678,7 @@ Try BottleCRM Free - diff --git a/src/routes/(site)/customization/+page.svelte b/src/routes/(site)/customization/+page.svelte index 8e885bf..d75d51c 100644 --- a/src/routes/(site)/customization/+page.svelte +++ b/src/routes/(site)/customization/+page.svelte @@ -832,7 +832,7 @@
diff --git a/src/routes/(site)/migration/+page.svelte b/src/routes/(site)/migration/+page.svelte index 88ad2dd..36f6534 100644 --- a/src/routes/(site)/migration/+page.svelte +++ b/src/routes/(site)/migration/+page.svelte @@ -646,7 +646,7 @@ class="flex-1 px-4 py-3 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-white" /> + + + + +{/if} + {#if showToast}
@@ -311,21 +369,23 @@
{#if lead.status !== 'CONVERTED'} -
- + +
+ {/if} Date: Wed, 18 Jun 2025 12:22:32 +0530 Subject: [PATCH 09/11] feat: refactor status and priority icon rendering for improved consistency across task and user components --- .../(app)/app/cases/[caseId]/+page.svelte | 5 +- src/routes/(app)/app/leads/open/+page.svelte | 12 ++- .../[opportunityId]/edit/+page.svelte | 2 +- .../(app)/app/tasks/calendar/+page.svelte | 17 +++- src/routes/(app)/app/tasks/list/+page.svelte | 92 +++++++++++-------- src/routes/(app)/app/users/+page.svelte | 14 ++- src/routes/(no-layout)/login/+page.svelte | 6 +- 7 files changed, 95 insertions(+), 53 deletions(-) diff --git a/src/routes/(app)/app/cases/[caseId]/+page.svelte b/src/routes/(app)/app/cases/[caseId]/+page.svelte index e305259..1fe08fd 100644 --- a/src/routes/(app)/app/cases/[caseId]/+page.svelte +++ b/src/routes/(app)/app/cases/[caseId]/+page.svelte @@ -31,6 +31,9 @@ default: return AlertCircle; } } + + // Reactive declarations for components + $: StatusIcon = getStatusIcon(data.caseItem.status);
@@ -185,7 +188,7 @@

Status

- + {data.caseItem.status.replace('_', ' ')} diff --git a/src/routes/(app)/app/leads/open/+page.svelte b/src/routes/(app)/app/leads/open/+page.svelte index 8fc8e7d..67d7ec7 100644 --- a/src/routes/(app)/app/leads/open/+page.svelte +++ b/src/routes/(app)/app/leads/open/+page.svelte @@ -392,7 +392,11 @@
- + {#snippet statusIcon(config)} + {@const StatusIcon = config.icon} + + {/snippet} + {@render statusIcon(statusConfig)} {lead.status} @@ -457,7 +461,11 @@
- + {#snippet statusIcon(config)} + {@const StatusIcon = config.icon} + + {/snippet} + {@render statusIcon(statusConfig)} {lead.status} diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte b/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte index f1d2ec1..7f23eb1 100644 --- a/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte +++ b/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte @@ -70,7 +70,7 @@
-
+

diff --git a/src/routes/(app)/app/tasks/calendar/+page.svelte b/src/routes/(app)/app/tasks/calendar/+page.svelte index 60de073..79ec205 100644 --- a/src/routes/(app)/app/tasks/calendar/+page.svelte +++ b/src/routes/(app)/app/tasks/calendar/+page.svelte @@ -212,10 +212,13 @@ >

{task.title}

- + {#snippet statusIcon(status)} + {@const StatusIcon = getStatusIcon(status)} + + {/snippet} + {@render statusIcon(task.status)}
{#if task.description} @@ -231,7 +234,11 @@ {task.priority === 'High' ? 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' : task.priority === 'Medium' ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'}"> - + {#snippet priorityIcon(priority)} + {@const PriorityIcon = getPriorityIcon(priority)} + + {/snippet} + {@render priorityIcon(task.priority)} {task.priority}

diff --git a/src/routes/(app)/app/tasks/list/+page.svelte b/src/routes/(app)/app/tasks/list/+page.svelte index b7c8372..b5b8f45 100644 --- a/src/routes/(app)/app/tasks/list/+page.svelte +++ b/src/routes/(app)/app/tasks/list/+page.svelte @@ -118,17 +118,20 @@
- + {#snippet statusIcon(status)} + {@const StatusIcon = getStatusIcon(status)} + + {/snippet} + {@render statusIcon(task.status)}
- + {#snippet priorityIcon(priority)} + {@const PriorityIcon = getPriorityIcon(priority)} + + {/snippet} + {@render priorityIcon(task.priority)}
- + {#snippet statusIconCard(status)} + {@const StatusIcon = getStatusIcon(status)} + + {/snippet} + {@render statusIconCard(task.status)} - + {#snippet priorityIconCard(priority)} + {@const PriorityIcon = getPriorityIcon(priority)} + + {/snippet} + {@render priorityIconCard(task.priority)} {#if user.isSelf} - + {#snippet roleIcon(role)} + {@const RoleIcon = roleIcons[role] || User} + + {/snippet} + {@render roleIcon(user.role)} {user.role} {:else} @@ -348,7 +352,11 @@ onclick={() => { users[i].editingRole = true }} title="Click to edit role" > - + {#snippet roleIcon(role)} + {@const RoleIcon = roleIcons[role] || User} + + {/snippet} + {@render roleIcon(user.role)} {user.role} @@ -366,7 +374,7 @@ method="POST" action="?/remove_user" class="inline" - on:submit={(e) => { + onsubmit={(e) => { if (!confirm('Remove this user from the organization?')) { e.preventDefault(); } diff --git a/src/routes/(no-layout)/login/+page.svelte b/src/routes/(no-layout)/login/+page.svelte index e337f54..c147575 100644 --- a/src/routes/(no-layout)/login/+page.svelte +++ b/src/routes/(no-layout)/login/+page.svelte @@ -106,7 +106,11 @@
- + {#snippet featureIcon(icon)} + {@const FeatureIcon = icon} + + {/snippet} + {@render featureIcon(feature.icon)}
{feature.text}
From fefe362543e79d45c8ce330026a253b1d4848d4a Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 18 Jun 2025 12:28:08 +0530 Subject: [PATCH 10/11] feat: remove 'Add Task' button for cleaner UI in contact details --- src/routes/(app)/app/contacts/[contactId]/+page.svelte | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/routes/(app)/app/contacts/[contactId]/+page.svelte b/src/routes/(app)/app/contacts/[contactId]/+page.svelte index 0828702..d4f2ea5 100644 --- a/src/routes/(app)/app/contacts/[contactId]/+page.svelte +++ b/src/routes/(app)/app/contacts/[contactId]/+page.svelte @@ -89,10 +89,6 @@
- Edit From edfd347bb70b255e08ba488c1b4c312ccdec879c Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 18 Jun 2025 12:59:12 +0530 Subject: [PATCH 11/11] feat: enhance lead editing with validation for required fields and owner ID, and update form data handling --- .../app/leads/[lead_id]/edit/+page.server.js | 105 +++++++++++++++--- .../app/leads/[lead_id]/edit/+page.svelte | 40 ++----- 2 files changed, 99 insertions(+), 46 deletions(-) diff --git a/src/routes/(app)/app/leads/[lead_id]/edit/+page.server.js b/src/routes/(app)/app/leads/[lead_id]/edit/+page.server.js index 32fbc87..f83fa62 100644 --- a/src/routes/(app)/app/leads/[lead_id]/edit/+page.server.js +++ b/src/routes/(app)/app/leads/[lead_id]/edit/+page.server.js @@ -1,5 +1,5 @@ import { error, redirect } from '@sveltejs/kit'; -import { PrismaClient } from '@prisma/client'; +import { PrismaClient, LeadStatus, LeadSource } from '@prisma/client'; const prisma = new PrismaClient(); @@ -20,7 +20,10 @@ export async function load({ params, locals }) { } const users = await prisma.userOrganization.findMany({ - where: { organizationId: org.id } + where: { organizationId: org.id }, + include: { + user: true + } }); return { @@ -37,6 +40,49 @@ export const actions = { const org = locals.org; const leadEmail = formData.get('email'); + const ownerId = formData.get('ownerId'); + const firstName = formData.get('firstName'); + const lastName = formData.get('lastName'); + + // Validate required fields + if (!firstName || typeof firstName !== 'string' || firstName.trim() === '') { + return { + success: false, + error: 'First name is required.' + }; + } + + if (!lastName || typeof lastName !== 'string' || lastName.trim() === '') { + return { + success: false, + error: 'Last name is required.' + }; + } + + if (!ownerId || typeof ownerId !== 'string') { + return { + success: false, + error: 'Owner ID is required.' + }; + } + + // Validate owner ID - ensure the user belongs to the organization + const ownerValidation = await prisma.userOrganization.findUnique({ + where: { + userId_organizationId: { + userId: ownerId, + organizationId: org.id, + }, + }, + select: { id: true } + }); + + if (!ownerValidation) { + return { + success: false, + error: 'Invalid owner selected. User is not part of this organization.' + }; + } // Check if leadEmail is a non-empty string before proceeding if (typeof leadEmail === 'string' && leadEmail.trim() !== '') { @@ -73,26 +119,49 @@ export const actions = { } } - const updatedLead = { - firstName: formData.get('firstName'), - lastName: formData.get('lastName'), - email: formData.get('email'), - phone: formData.get('phone'), - company: formData.get('company'), - title: formData.get('title'), - status: formData.get('status'), - leadSource: formData.get('leadSource') || null, - industry: formData.get('industry') || null, - rating: formData.get('rating') || null, - description: formData.get('description') || null, - ownerId: formData.get('ownerId'), - organizationId: org.id // Always set from session - }; + // Get and validate form data + const statusValue = formData.get('status')?.toString() || 'NEW'; + const leadSourceValue = formData.get('leadSource')?.toString(); + + // Simple string validation - Prisma will validate the enum at runtime + const validStatuses = ['NEW', 'PENDING', 'CONTACTED', 'QUALIFIED', 'UNQUALIFIED', 'CONVERTED']; + const validSources = ['WEB', 'PHONE_INQUIRY', 'PARTNER_REFERRAL', 'COLD_CALL', 'TRADE_SHOW', 'EMPLOYEE_REFERRAL', 'ADVERTISEMENT', 'OTHER']; + if (!validStatuses.includes(statusValue)) { + return { + success: false, + error: 'Invalid lead status provided.' + }; + } + + if (leadSourceValue && !validSources.includes(leadSourceValue)) { + return { + success: false, + error: 'Invalid lead source provided.' + }; + } + try { + // Use the correct Prisma update method with proper typing await prisma.lead.update({ where: { id: lead_id }, - data: updatedLead + data: { + firstName: firstName.trim(), + lastName: lastName.trim(), + email: formData.get('email')?.toString() || null, + phone: formData.get('phone')?.toString() || null, + company: formData.get('company')?.toString() || null, + title: formData.get('title')?.toString() || null, + industry: formData.get('industry')?.toString() || null, + rating: formData.get('rating')?.toString() || null, + description: formData.get('description')?.toString() || null, + ownerId: ownerId, + organizationId: org.id, + // @ts-ignore - Bypassing TypeScript enum checking for validated enum values + status: statusValue, + // @ts-ignore - Bypassing TypeScript enum checking for validated enum values + leadSource: leadSourceValue || null + } }); return { success: true }; diff --git a/src/routes/(app)/app/leads/[lead_id]/edit/+page.svelte b/src/routes/(app)/app/leads/[lead_id]/edit/+page.svelte index d5e1b6f..0b07e24 100644 --- a/src/routes/(app)/app/leads/[lead_id]/edit/+page.svelte +++ b/src/routes/(app)/app/leads/[lead_id]/edit/+page.svelte @@ -1,8 +1,8 @@ -