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..96687b7 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({});
@@ -86,10 +119,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',
@@ -205,8 +242,8 @@
-
-
+
+
@@ -238,38 +275,48 @@
-
+
{#if userDropdownOpen}
-
{/if}
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}
+
+
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+ Create New Account
+
+
Add a new company or organization to your CRM
+
+
+
+
+
+
+ {#if form?.error}
+
+
+
+
Error:
+
{form.error}
+
+
+ {/if}
+
+
+
+
+
\ 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
});
}