Skip to content
Merged

Dev #51

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions src/lib/data/accountOwnership.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const accountOwnership = [
['', 'Select Ownership'],
['PUBLIC', 'Public'],
['PRIVATE', 'Private'],
['SUBSIDIARY', 'Subsidiary'],
['NON_PROFIT', 'Non-Profit'],
['GOVERNMENT', 'Government'],
['OTHER', 'Other']
];
9 changes: 9 additions & 0 deletions src/lib/data/accountTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const accountTypes = [
['', 'Select Type'],
['CUSTOMER', 'Customer'],
['PARTNER', 'Partner'],
['PROSPECT', 'Prospect'],
['VENDOR', 'Vendor'],
['COMPETITOR', 'Competitor'],
['OTHER', 'Other']
];
44 changes: 44 additions & 0 deletions src/lib/data/countries.js
Original file line number Diff line number Diff line change
@@ -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']
];
8 changes: 8 additions & 0 deletions src/lib/data/index.js
Original file line number Diff line number Diff line change
@@ -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';
16 changes: 16 additions & 0 deletions src/lib/data/industries.js
Original file line number Diff line number Diff line change
@@ -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']
];
18 changes: 18 additions & 0 deletions src/lib/data/leadSources.js
Original file line number Diff line number Diff line change
@@ -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']
];
7 changes: 7 additions & 0 deletions src/lib/data/leadStatuses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const leadStatuses = [
['NEW', 'New'],
['PENDING', 'Pending'],
['CONTACTED', 'Contacted'],
['QUALIFIED', 'Qualified'],
['UNQUALIFIED', 'Unqualified']
];
6 changes: 6 additions & 0 deletions src/lib/data/ratings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const ratings = [
['', 'Select Rating'],
['HOT', '🔥 Hot'],
['WARM', '🟡 Warm'],
['COLD', '🟦 Cold']
];
91 changes: 69 additions & 22 deletions src/routes/(app)/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

let isDark = $state(false);
let userDropdownOpen = $state(false);
let dropdownRef = $state();

const closeDrawer = () => {
drawerHidden = true;
Expand All @@ -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({});

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -205,8 +242,8 @@
</nav>
</div>

<!-- User profile section - moved to bottom -->
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<!-- settings section -->
<div class="p-4 border-t border-gray-200 dark:border-gray-700" bind:this={dropdownRef}>
<div class="flex items-center gap-3 mb-3">
<img class="w-10 h-10 rounded-lg object-cover" src={user.profilePhoto} alt="User avatar" />
<div class="flex-1 min-w-0">
Expand Down Expand Up @@ -238,38 +275,48 @@
</button>
</div>

<!-- User dropdown menu -->
<!-- settings dropdown menu -->
{#if userDropdownOpen}
<div class="mt-3 p-1 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<a
href="/app/profile"
class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-700 rounded transition-colors"
<div
class="mt-3 p-1 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
onclick={handleDropdownClick}
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleDropdownClick(e); }}
tabindex="0"
role="menu"
>
<button
type="button"
onclick={(e) => handleSettingsLinkClick(e, '/app/profile')}
class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-700 rounded transition-colors w-full text-left"
>
<User class="w-4 h-4" />
Profile
</a>
<a
href="/app/users"
class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-700 rounded transition-colors"
</button>
<button
type="button"
onclick={(e) => handleSettingsLinkClick(e, '/app/users')}
class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-700 rounded transition-colors w-full text-left"
>
<Users class="w-4 h-4" />
Users
</a>
<a
href="/org"
class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-700 rounded transition-colors"
</button>
<button
type="button"
onclick={(e) => handleSettingsLinkClick(e, '/org')}
class="flex items-center gap-3 px-3 py-2 text-sm text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-700 rounded transition-colors w-full text-left"
>
<Building class="w-4 h-4" />
Organizations
</a>
</button>
<hr class="my-1 border-gray-200 dark:border-gray-600" />
<a
href="/logout"
class="flex items-center gap-3 px-3 py-2 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20 rounded transition-colors"
<button
type="button"
onclick={(e) => handleSettingsLinkClick(e, '/logout')}
class="flex items-center gap-3 px-3 py-2 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20 rounded transition-colors w-full text-left"
>
<LogOut class="w-4 h-4" />
Sign out
</a>
</button>
</div>
{/if}
</div>
Expand Down
106 changes: 106 additions & 0 deletions src/routes/(app)/app/accounts/new/+page.server.js
Original file line number Diff line number Diff line change
@@ -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
});
}
}
};
Loading