Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ user types we have
## Project Context

BottleCRM is a modern CRM application built with:
- **Framework**: SvelteKit 2.21.x, Svelte 5.x, Prisma
- **Framework**: SvelteKit 2.21.x, Svelte 5.1, Prisma
- **Styling**: tailwind 4.1.x css
- **Database**: postgresql
- **Icons**: lucide icons
- **Form Validation**: zod

## Important Notes
- We need to ensure access control is strictly enforced based on user roles.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@prisma/client": "6.5.0",
"axios": "^1.9.0",
"date-fns": "^4.1.0",
"libphonenumber-js": "^1.12.9",
"marked": "^15.0.12",
"svelte-highlight": "^7.8.3",
"svelte-meta-tags": "^4.4.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions prisma/migrations/20250618024526_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- You are about to drop the column `department` on the `User` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "department";
1 change: 0 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ model User {
updatedAt DateTime @updatedAt
profilePhoto String?
phone String?
department String?
isActive Boolean @default(true)
lastLogin DateTime?
accounts Account[]
Expand Down
74 changes: 74 additions & 0 deletions src/lib/utils/phone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';

/**
* Validates a phone number and returns validation result
* @param {string} phoneNumber - The phone number to validate
* @param {string} defaultCountry - Default country code (e.g., 'US')
* @returns {{ isValid: boolean, formatted?: string, error?: string }}
*/
export function validatePhoneNumber(phoneNumber, defaultCountry = 'US') {
if (!phoneNumber || phoneNumber.trim() === '') {
return { isValid: true }; // Allow empty phone numbers
}

try {
// @ts-ignore - defaultCountry is a valid CountryCode
const isValid = isValidPhoneNumber(phoneNumber, { defaultCountry });

if (!isValid) {
return {
isValid: false,
error: 'Please enter a valid phone number'
};
}

// Parse and format the phone number
// @ts-ignore - defaultCountry is a valid CountryCode
const parsed = parsePhoneNumber(phoneNumber, { defaultCountry });
return {
isValid: true,
formatted: parsed.formatInternational()
};
} catch (error) {
return {
isValid: false,
error: 'Please enter a valid phone number'
};
}
}

/**
* Formats a phone number for display
* @param {string} phoneNumber - The phone number to format
* @param {string} defaultCountry - Default country code
* @returns {string} Formatted phone number or original if invalid
*/
export function formatPhoneNumber(phoneNumber, defaultCountry = 'US') {
if (!phoneNumber) return '';

try {
// @ts-ignore - defaultCountry is a valid CountryCode
const parsed = parsePhoneNumber(phoneNumber, { defaultCountry });
return parsed.formatInternational();
} catch {
return phoneNumber; // Return original if parsing fails
}
}

/**
* Formats a phone number for storage (E.164 format)
* @param {string} phoneNumber - The phone number to format
* @param {string} defaultCountry - Default country code
* @returns {string} E.164 formatted phone number or original if invalid
*/
export function formatPhoneForStorage(phoneNumber, defaultCountry = 'US') {
if (!phoneNumber) return '';

try {
// @ts-ignore - defaultCountry is a valid CountryCode
const parsed = parsePhoneNumber(phoneNumber, { defaultCountry });
return parsed.format('E.164');
} catch {
return phoneNumber; // Return original if parsing fails
}
}
14 changes: 13 additions & 1 deletion src/routes/(app)/app/accounts/new/+page.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
accountTypes,
Expand Down Expand Up @@ -44,13 +45,24 @@ export const actions = {
return fail(400, { error: 'Account name 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 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,
phone: formattedPhone,
street: formData.get('street')?.toString() || null,
city: formData.get('city')?.toString() || null,
state: formData.get('state')?.toString() || null,
Expand Down
38 changes: 19 additions & 19 deletions src/routes/(app)/app/accounts/new/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@
<p class="text-sm font-medium {toastType === 'success' ? 'text-green-800 dark:text-green-200' : 'text-red-800 dark:text-red-200'}">{toastMessage}</p>
</div>
<button
on:click={() => showToast = false}
onclick={() => showToast = false}
class="ml-auto -mx-1.5 -my-1.5 rounded-lg p-1.5 {toastType === 'success' ? 'text-green-500 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-800/30' : 'text-red-500 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-800/30'}">
<X class="w-4 h-4" />
</button>
Expand Down Expand Up @@ -241,7 +241,7 @@
name="name"
type="text"
bind:value={formData.name}
on:input={handleChange}
oninput={handleChange}
placeholder="Enter account 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.name ? 'border-red-500 dark:border-red-400 ring-1 ring-red-500 dark:ring-red-400' : ''}" />
Expand All @@ -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]}
<option value={value}>{label}</option>
Expand All @@ -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]}
<option value={value}>{label}</option>
Expand All @@ -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]}
<option value={value}>{label}</option>
Expand All @@ -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]}
<option value={value}>{label}</option>
Expand Down Expand Up @@ -342,7 +342,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}
Expand All @@ -361,7 +361,7 @@
name="phone"
type="tel"
bind:value={formData.phone}
on:input={handleChange}
oninput={handleChange}
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' : ''}" />
{#if errors.phone}
Expand Down Expand Up @@ -392,7 +392,7 @@
name="street"
type="text"
bind:value={formData.street}
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" />
</div>
Expand All @@ -404,7 +404,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" />
</div>
Expand All @@ -416,7 +416,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" />
</div>
Expand All @@ -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" />
</div>
Expand All @@ -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]}
<option value={value}>{label}</option>
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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" />
</div>
Expand All @@ -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" />
</div>
Expand All @@ -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"></textarea>
Expand All @@ -564,7 +564,7 @@
<div class="flex justify-end gap-4">
<button
type="button"
on:click={() => goto('/app/accounts')}
onclick={() => goto('/app/accounts')}
disabled={isSubmitting}
class="px-6 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2">
<X class="w-4 h-4" />
Expand Down
Loading