From f5dea13acbba4aa77906b73681de65c1dc29e22c Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 18 Jun 2025 06:28:54 +0530 Subject: [PATCH 1/3] Refactor data structure and form handling for account and lead creation - Updated icons from FontAwesome to Lucide in the project documentation. - Added new data files for account ownership, account types, countries, industries, lead sources, lead statuses, and ratings. - Re-exported new data constants in index.js for easier imports. - Enhanced the Sidebar component to include dropdowns for account management. - Implemented a new page for creating accounts with comprehensive form validation and submission handling. - Integrated dropdown data for industries, account types, ownership, ratings, and countries in the account creation form. - Updated lead creation logic to utilize centralized data constants for industries, lead sources, lead statuses, and countries. --- .github/copilot-instructions.md | 5 +- src/lib/data/accountOwnership.js | 9 + src/lib/data/accountTypes.js | 9 + src/lib/data/countries.js | 44 ++ src/lib/data/index.js | 8 + src/lib/data/industries.js | 16 + src/lib/data/leadSources.js | 18 + src/lib/data/leadStatuses.js | 7 + src/lib/data/ratings.js | 6 + src/routes/(app)/Sidebar.svelte | 8 +- .../(app)/app/accounts/new/+page.server.js | 106 ++++ .../(app)/app/accounts/new/+page.svelte | 590 ++++++++++++++++++ .../(app)/app/leads/new/+page.server.js | 68 +- 13 files changed, 839 insertions(+), 55 deletions(-) create mode 100644 src/lib/data/accountOwnership.js create mode 100644 src/lib/data/accountTypes.js create mode 100644 src/lib/data/countries.js create mode 100644 src/lib/data/index.js create mode 100644 src/lib/data/industries.js create mode 100644 src/lib/data/leadSources.js create mode 100644 src/lib/data/leadStatuses.js create mode 100644 src/lib/data/ratings.js create mode 100644 src/routes/(app)/app/accounts/new/+page.server.js create mode 100644 src/routes/(app)/app/accounts/new/+page.svelte diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4e66d70..b0eba41 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -17,8 +17,9 @@ BottleCRM is a modern CRM application built with: - **Framework**: SvelteKit 2.21.x, Svelte 5.x, Prisma - **Styling**: tailwind 4.1.x css - **Database**: postgresql -- **Icons**: fontawesome, we are migrating to lucide icons +- **Icons**: lucide icons ## Important Notes -- We need to ensure access control is strictly enforced based on user roles. No record is accessible unless the user or the org has the appropriate permissions. +- We need to ensure access control is strictly enforced based on user roles. +- No record should be accessible unless the user or the org has the appropriate permissions. - When implementing forms in sveltekit A form label must be associated with a control \ No newline at end of file diff --git a/src/lib/data/accountOwnership.js b/src/lib/data/accountOwnership.js new file mode 100644 index 0000000..203fbcf --- /dev/null +++ b/src/lib/data/accountOwnership.js @@ -0,0 +1,9 @@ +export const accountOwnership = [ + ['', 'Select Ownership'], + ['PUBLIC', 'Public'], + ['PRIVATE', 'Private'], + ['SUBSIDIARY', 'Subsidiary'], + ['NON_PROFIT', 'Non-Profit'], + ['GOVERNMENT', 'Government'], + ['OTHER', 'Other'] +]; diff --git a/src/lib/data/accountTypes.js b/src/lib/data/accountTypes.js new file mode 100644 index 0000000..6cbaeee --- /dev/null +++ b/src/lib/data/accountTypes.js @@ -0,0 +1,9 @@ +export const accountTypes = [ + ['', 'Select Type'], + ['CUSTOMER', 'Customer'], + ['PARTNER', 'Partner'], + ['PROSPECT', 'Prospect'], + ['VENDOR', 'Vendor'], + ['COMPETITOR', 'Competitor'], + ['OTHER', 'Other'] +]; diff --git a/src/lib/data/countries.js b/src/lib/data/countries.js new file mode 100644 index 0000000..a38992a --- /dev/null +++ b/src/lib/data/countries.js @@ -0,0 +1,44 @@ +export const countries = [ + ['', 'Select Country'], + ['US', 'United States'], + ['UK', 'United Kingdom'], + ['CA', 'Canada'], + ['AU', 'Australia'], + ['IN', 'India'], + ['DE', 'Germany'], + ['FR', 'France'], + ['JP', 'Japan'], + ['CN', 'China'], + ['BR', 'Brazil'], + ['MX', 'Mexico'], + ['IT', 'Italy'], + ['ES', 'Spain'], + ['NL', 'Netherlands'], + ['SE', 'Sweden'], + ['NO', 'Norway'], + ['DK', 'Denmark'], + ['FI', 'Finland'], + ['CH', 'Switzerland'], + ['AT', 'Austria'], + ['BE', 'Belgium'], + ['IE', 'Ireland'], + ['PL', 'Poland'], + ['RU', 'Russia'], + ['KR', 'South Korea'], + ['SG', 'Singapore'], + ['TH', 'Thailand'], + ['MY', 'Malaysia'], + ['ID', 'Indonesia'], + ['PH', 'Philippines'], + ['VN', 'Vietnam'], + ['NZ', 'New Zealand'], + ['ZA', 'South Africa'], + ['EG', 'Egypt'], + ['NG', 'Nigeria'], + ['KE', 'Kenya'], + ['AR', 'Argentina'], + ['CL', 'Chile'], + ['CO', 'Colombia'], + ['PE', 'Peru'], + ['OTHER', 'Other'] +]; diff --git a/src/lib/data/index.js b/src/lib/data/index.js new file mode 100644 index 0000000..f5efe2f --- /dev/null +++ b/src/lib/data/index.js @@ -0,0 +1,8 @@ +// Re-export all data constants for easy importing +export { industries } from './industries.js'; +export { accountTypes } from './accountTypes.js'; +export { accountOwnership } from './accountOwnership.js'; +export { ratings } from './ratings.js'; +export { countries } from './countries.js'; +export { leadSources } from './leadSources.js'; +export { leadStatuses } from './leadStatuses.js'; diff --git a/src/lib/data/industries.js b/src/lib/data/industries.js new file mode 100644 index 0000000..aa3dc88 --- /dev/null +++ b/src/lib/data/industries.js @@ -0,0 +1,16 @@ +export const industries = [ + ['', 'Select Industry'], + ['TECHNOLOGY', 'Technology'], + ['HEALTHCARE', 'Healthcare'], + ['FINANCE', 'Finance'], + ['EDUCATION', 'Education'], + ['RETAIL', 'Retail'], + ['MANUFACTURING', 'Manufacturing'], + ['ENERGY', 'Energy'], + ['REAL_ESTATE', 'Real Estate'], + ['CONSTRUCTION', 'Construction'], + ['TRANSPORTATION', 'Transportation'], + ['HOSPITALITY', 'Hospitality'], + ['AGRICULTURE', 'Agriculture'], + ['OTHER', 'Other'] +]; diff --git a/src/lib/data/leadSources.js b/src/lib/data/leadSources.js new file mode 100644 index 0000000..bb3fb67 --- /dev/null +++ b/src/lib/data/leadSources.js @@ -0,0 +1,18 @@ +export const leadSources = [ + ['', 'Select Source'], + ['WEB', 'Website'], + ['PHONE_INQUIRY', 'Phone Inquiry'], + ['PARTNER_REFERRAL', 'Partner Referral'], + ['COLD_CALL', 'Cold Call'], + ['TRADE_SHOW', 'Trade Show'], + ['EMPLOYEE_REFERRAL', 'Employee Referral'], + ['ADVERTISEMENT', 'Advertisement'], + ['SOCIAL_MEDIA', 'Social Media'], + ['EMAIL_CAMPAIGN', 'Email Campaign'], + ['WEBINAR', 'Webinar'], + ['CONTENT_MARKETING', 'Content Marketing'], + ['SEO', 'SEO/Organic Search'], + ['PPC', 'Pay-Per-Click Advertising'], + ['DIRECT_MAIL', 'Direct Mail'], + ['OTHER', 'Other'] +]; diff --git a/src/lib/data/leadStatuses.js b/src/lib/data/leadStatuses.js new file mode 100644 index 0000000..9653611 --- /dev/null +++ b/src/lib/data/leadStatuses.js @@ -0,0 +1,7 @@ +export const leadStatuses = [ + ['NEW', 'New'], + ['PENDING', 'Pending'], + ['CONTACTED', 'Contacted'], + ['QUALIFIED', 'Qualified'], + ['UNQUALIFIED', 'Unqualified'] +]; diff --git a/src/lib/data/ratings.js b/src/lib/data/ratings.js new file mode 100644 index 0000000..b93a611 --- /dev/null +++ b/src/lib/data/ratings.js @@ -0,0 +1,6 @@ +export const ratings = [ + ['', 'Select Rating'], + ['HOT', '🔥 Hot'], + ['WARM', '🟡 Warm'], + ['COLD', '🟦 Cold'] +]; diff --git a/src/routes/(app)/Sidebar.svelte b/src/routes/(app)/Sidebar.svelte index e0bb26e..3df10d2 100644 --- a/src/routes/(app)/Sidebar.svelte +++ b/src/routes/(app)/Sidebar.svelte @@ -86,10 +86,14 @@ ] }, { - href: '/app/accounts', + key: 'accounts', label: 'Accounts', icon: Building, - type: 'link' + type: 'dropdown', + children: [ + { href: '/app/accounts', label: 'All Accounts', icon: List }, + { href: '/app/accounts/new', label: 'New Account', icon: Plus } + ] }, { key: 'opportunities', diff --git a/src/routes/(app)/app/accounts/new/+page.server.js b/src/routes/(app)/app/accounts/new/+page.server.js new file mode 100644 index 0000000..99f69c7 --- /dev/null +++ b/src/routes/(app)/app/accounts/new/+page.server.js @@ -0,0 +1,106 @@ +import { env } from '$env/dynamic/private'; +import { redirect } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; +import { fail } from '@sveltejs/kit'; +import { + industries, + accountTypes, + accountOwnership, + ratings, + countries +} from '$lib/data/index.js'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals }) { + const user = locals.user; + const org = locals.org; + + // Get data for dropdowns + return { + data: { + industries, + accountTypes, + accountOwnership, + ratings, + countries + } + }; +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals }) => { + // Get user and org from locals + const user = locals.user; + const org = locals.org; + + // Get the submitted form data + const formData = await request.formData(); + + // Extract and validate required fields + const name = formData.get('name')?.toString().trim(); + + if (!name) { + return fail(400, { error: 'Account name is required' }); + } + + // Extract all form fields + const accountData = { + name, + type: formData.get('type')?.toString() || null, + industry: formData.get('industry')?.toString() || null, + website: formData.get('website')?.toString() || null, + phone: formData.get('phone')?.toString() || null, + street: formData.get('street')?.toString() || null, + city: formData.get('city')?.toString() || null, + state: formData.get('state')?.toString() || null, + postalCode: formData.get('postalCode')?.toString() || null, + country: formData.get('country')?.toString() || null, + description: formData.get('description')?.toString() || null, + numberOfEmployees: formData.get('numberOfEmployees') ? + parseInt(formData.get('numberOfEmployees')?.toString() || '0') : null, + annualRevenue: formData.get('annualRevenue') ? + parseFloat(formData.get('annualRevenue')?.toString() || '0') : null, + accountOwnership: formData.get('accountOwnership')?.toString() || null, + tickerSymbol: formData.get('tickerSymbol')?.toString() || null, + rating: formData.get('rating')?.toString() || null, + sicCode: formData.get('sicCode')?.toString() || null + }; + + try { + // Create new account in the database + const account = await prisma.account.create({ + data: { + ...accountData, + owner: { + connect: { + id: user.id + } + }, + organization: { + connect: { + id: org.id + } + } + } + }); + + // Return success instead of redirecting + return { + status: 'success', + message: 'Account created successfully', + account: { + id: account.id, + name: account.name + } + }; + + } catch (err) { + console.error('Error creating account:', err); + return fail(500, { + error: 'Failed to create account: ' + (err instanceof Error ? err.message : 'Unknown error'), + values: accountData // Return entered values so the form can be repopulated + }); + } + } +}; \ No newline at end of file diff --git a/src/routes/(app)/app/accounts/new/+page.svelte b/src/routes/(app)/app/accounts/new/+page.svelte new file mode 100644 index 0000000..130c215 --- /dev/null +++ b/src/routes/(app)/app/accounts/new/+page.svelte @@ -0,0 +1,590 @@ + + + +
+ + {#if showToast} +
+
+
+ {#if toastType === 'success'} + + {:else} + + {/if} +
+
+

{toastMessage}

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

+ + Create New Account +

+

Add a new company or organization to your CRM

+
+
+
+
+ + + {#if form?.error} +
+
+ + Error: + {form.error} +
+
+ {/if} + + +
{ + if (!validateForm()) { + cancel(); + return; + } + + isSubmitting = true; + + return async ({ result }) => { + isSubmitting = false; + + if (result.type === 'success') { + showNotification('Account created successfully!', 'success'); + resetForm(); + setTimeout(() => goto('/app/accounts'), 1500); + } else if (result.type === 'failure') { + showNotification(result.data?.error || 'Failed to create account', 'error'); + } + }; + }} class="space-y-6"> + + +
+
+

+ + Basic Information +

+
+
+
+ +
+ + + {#if errors.name} +

{errors.name}

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

+ + Contact Information +

+
+
+
+ +
+ + + {#if errors.website} +

{errors.website}

+ {/if} +
+ + +
+ + + {#if errors.phone} +

{errors.phone}

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

+ + Address Information +

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

+ + Company Details +

+
+
+
+ +
+ + + {#if errors.numberOfEmployees} +

{errors.numberOfEmployees}

+ {/if} +
+ + +
+ + + {#if errors.annualRevenue} +

{errors.annualRevenue}

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

Additional Details

+
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/src/routes/(app)/app/leads/new/+page.server.js b/src/routes/(app)/app/leads/new/+page.server.js index 40376cf..47943e3 100644 --- a/src/routes/(app)/app/leads/new/+page.server.js +++ b/src/routes/(app)/app/leads/new/+page.server.js @@ -2,6 +2,12 @@ import { env } from '$env/dynamic/private'; import { redirect } from '@sveltejs/kit'; import prisma from '$lib/prisma'; import { fail } from '@sveltejs/kit'; +import { + industries, + leadSources, + leadStatuses, + countries +} from '$lib/data/index.js'; /** @type {import('./$types').PageServerLoad} */ export async function load({ locals }) { @@ -11,51 +17,10 @@ export async function load({ locals }) { // Get data for dropdowns return { data: { - industries: [ - ['', 'Select Industry'], - ['TECHNOLOGY', 'Technology'], - ['HEALTHCARE', 'Healthcare'], - ['FINANCE', 'Finance'], - ['EDUCATION', 'Education'], - ['RETAIL', 'Retail'], - ['MANUFACTURING', 'Manufacturing'], - ['ENERGY', 'Energy'], - ['REAL_ESTATE', 'Real Estate'], - ['CONSTRUCTION', 'Construction'], - ['TRANSPORTATION', 'Transportation'], - ['HOSPITALITY', 'Hospitality'], - ['AGRICULTURE', 'Agriculture'], - ['OTHER', 'Other'] - ], - status: Object.entries({ - 'NEW': 'New', - 'PENDING': 'Pending', - 'CONTACTED': 'Contacted', - 'QUALIFIED': 'Qualified', - 'UNQUALIFIED': 'Unqualified' - }), - source: Object.entries({ - 'WEB': 'Website', - 'PHONE_INQUIRY': 'Phone Inquiry', - 'PARTNER_REFERRAL': 'Partner Referral', - 'COLD_CALL': 'Cold Call', - 'TRADE_SHOW': 'Trade Show', - 'EMPLOYEE_REFERRAL': 'Employee Referral', - 'ADVERTISEMENT': 'Advertisement', - 'OTHER': 'Other' - }), - countries: [ - ['', 'Select Country'], - ['US', 'United States'], - ['UK', 'United Kingdom'], - ['CA', 'Canada'], - ['AU', 'Australia'], - ['IN', 'India'], - ['DE', 'Germany'], - ['FR', 'France'], - ['JP', 'Japan'], - ['OTHER', 'Other'] - ] + industries, + status: leadStatuses, + source: leadSources, + countries } }; } @@ -97,18 +62,18 @@ export const actions = { email: email || null, phone: formData.get('phone')?.toString() || null, company: formData.get('company')?.toString() || null, - status: formData.get('status')?.toString() || 'PENDING', + status: (formData.get('status')?.toString() || 'PENDING'), leadSource: formData.get('source')?.toString() || null, industry: formData.get('industry')?.toString() || null, description: formData.get('description')?.toString() || null, // Store opportunity amount in description since it's not in the Lead schema opportunityAmount: formData.get('opportunity_amount') ? - parseFloat(formData.get('opportunity_amount')) : null, + parseFloat(formData.get('opportunity_amount')?.toString() || '0') : null, // Store probability in description since it's not in the Lead schema probability: formData.get('probability') ? - parseFloat(formData.get('probability')) : null, + parseFloat(formData.get('probability')?.toString() || '0') : null, // Address fields street: formData.get('street')?.toString() || null, @@ -124,7 +89,7 @@ export const actions = { try { // Prepare basic lead data that matches the Prisma schema - let leadCreateData = { + const leadCreateData = { firstName: leadData.firstName, lastName: leadData.lastName, email: leadData.email, @@ -132,7 +97,7 @@ export const actions = { company: leadData.company, title: leadData.title, status: leadData.status, - ...(leadData.leadSource ? { leadSource: leadData.leadSource } : {}), + leadSource: leadData.leadSource || null, industry: leadData.industry, description: leadData.description || '', rating: null, // This is in the schema @@ -153,6 +118,7 @@ export const actions = { // Create new lead in the database const lead = await prisma.lead.create({ + // @ts-ignore - status is a valid LeadStatus enum value data: leadCreateData }); @@ -169,7 +135,7 @@ export const actions = { } catch (err) { console.error('Error creating lead:', err); return fail(500, { - error: 'Failed to create lead: ' + err.message, + error: 'Failed to create lead: ' + (err instanceof Error ? err.message : 'Unknown error'), values: leadData // Return entered values so the form can be repopulated }); } From b7f19c230ccec7803e9401f12ca035f82e23708c Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 18 Jun 2025 07:50:46 +0530 Subject: [PATCH 2/3] feat: implement user dropdown functionality with click outside detection --- src/routes/(app)/Sidebar.svelte | 80 ++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/src/routes/(app)/Sidebar.svelte b/src/routes/(app)/Sidebar.svelte index 3df10d2..e892545 100644 --- a/src/routes/(app)/Sidebar.svelte +++ b/src/routes/(app)/Sidebar.svelte @@ -32,6 +32,7 @@ let isDark = $state(false); let userDropdownOpen = $state(false); + let dropdownRef = $state(); const closeDrawer = () => { drawerHidden = true; @@ -46,6 +47,38 @@ userDropdownOpen = !userDropdownOpen; }; + const handleSettingsLinkClick = (event, href) => { + event.preventDefault(); + event.stopPropagation(); + + // Close the dropdown + userDropdownOpen = false; + + // Navigate to the intended URL + window.location.href = href; + }; + + const handleDropdownClick = (event) => { + // Prevent clicks inside dropdown from bubbling up + event.stopPropagation(); + }; + + const handleClickOutside = (event) => { + if (userDropdownOpen && dropdownRef && !dropdownRef.contains(event.target)) { + userDropdownOpen = false; + } + }; + + // Add click outside listener + $effect(() => { + if (typeof document !== 'undefined') { + document.addEventListener('click', handleClickOutside); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + } + }); + let mainSidebarUrl = $derived($page.url.pathname); let openDropdowns = $state({}); @@ -209,8 +242,8 @@ - -
+ +
From b8c22192c3e71ed5c99a1195bf756e64d7619c3e Mon Sep 17 00:00:00 2001 From: Ashwin Date: Wed, 18 Jun 2025 07:55:24 +0530 Subject: [PATCH 3/3] feat: enhance accessibility for dropdown menu with keyboard navigation support --- src/routes/(app)/Sidebar.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/routes/(app)/Sidebar.svelte b/src/routes/(app)/Sidebar.svelte index e892545..96687b7 100644 --- a/src/routes/(app)/Sidebar.svelte +++ b/src/routes/(app)/Sidebar.svelte @@ -280,6 +280,9 @@
{ if (e.key === 'Enter' || e.key === ' ') handleDropdownClick(e); }} + tabindex="0" + role="menu" >