Skip to content

Commit b2d339e

Browse files
authored
Merge pull request #52 from MicroPyramid/dev
bug fix
2 parents b0d184a + edfd347 commit b2d339e

File tree

47 files changed

+1610
-352
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1610
-352
lines changed

.github/copilot-instructions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ user types we have
1414
## Project Context
1515

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

2223
## Important Notes
2324
- We need to ensure access control is strictly enforced based on user roles.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@prisma/client": "6.5.0",
4747
"axios": "^1.9.0",
4848
"date-fns": "^4.1.0",
49+
"libphonenumber-js": "^1.12.9",
4950
"marked": "^15.0.12",
5051
"svelte-highlight": "^7.8.3",
5152
"svelte-meta-tags": "^4.4.0",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `department` on the `User` table. All the data in the column will be lost.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "User" DROP COLUMN "department";

prisma/schema.prisma

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ model User {
2222
updatedAt DateTime @updatedAt
2323
profilePhoto String?
2424
phone String?
25-
department String?
2625
isActive Boolean @default(true)
2726
lastLogin DateTime?
2827
accounts Account[]

src/lib/utils/phone.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
2+
3+
/**
4+
* Validates a phone number and returns validation result
5+
* @param {string} phoneNumber - The phone number to validate
6+
* @param {string} defaultCountry - Default country code (e.g., 'US')
7+
* @returns {{ isValid: boolean, formatted?: string, error?: string }}
8+
*/
9+
export function validatePhoneNumber(phoneNumber, defaultCountry = 'US') {
10+
if (!phoneNumber || phoneNumber.trim() === '') {
11+
return { isValid: true }; // Allow empty phone numbers
12+
}
13+
14+
try {
15+
// @ts-ignore - defaultCountry is a valid CountryCode
16+
const isValid = isValidPhoneNumber(phoneNumber, { defaultCountry });
17+
18+
if (!isValid) {
19+
return {
20+
isValid: false,
21+
error: 'Please enter a valid phone number'
22+
};
23+
}
24+
25+
// Parse and format the phone number
26+
// @ts-ignore - defaultCountry is a valid CountryCode
27+
const parsed = parsePhoneNumber(phoneNumber, { defaultCountry });
28+
return {
29+
isValid: true,
30+
formatted: parsed.formatInternational()
31+
};
32+
} catch (error) {
33+
return {
34+
isValid: false,
35+
error: 'Please enter a valid phone number'
36+
};
37+
}
38+
}
39+
40+
/**
41+
* Formats a phone number for display
42+
* @param {string} phoneNumber - The phone number to format
43+
* @param {string} defaultCountry - Default country code
44+
* @returns {string} Formatted phone number or original if invalid
45+
*/
46+
export function formatPhoneNumber(phoneNumber, defaultCountry = 'US') {
47+
if (!phoneNumber) return '';
48+
49+
try {
50+
// @ts-ignore - defaultCountry is a valid CountryCode
51+
const parsed = parsePhoneNumber(phoneNumber, { defaultCountry });
52+
return parsed.formatInternational();
53+
} catch {
54+
return phoneNumber; // Return original if parsing fails
55+
}
56+
}
57+
58+
/**
59+
* Formats a phone number for storage (E.164 format)
60+
* @param {string} phoneNumber - The phone number to format
61+
* @param {string} defaultCountry - Default country code
62+
* @returns {string} E.164 formatted phone number or original if invalid
63+
*/
64+
export function formatPhoneForStorage(phoneNumber, defaultCountry = 'US') {
65+
if (!phoneNumber) return '';
66+
67+
try {
68+
// @ts-ignore - defaultCountry is a valid CountryCode
69+
const parsed = parsePhoneNumber(phoneNumber, { defaultCountry });
70+
return parsed.format('E.164');
71+
} catch {
72+
return phoneNumber; // Return original if parsing fails
73+
}
74+
}

src/routes/(app)/app/accounts/new/+page.server.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { env } from '$env/dynamic/private';
22
import { redirect } from '@sveltejs/kit';
33
import prisma from '$lib/prisma';
44
import { fail } from '@sveltejs/kit';
5+
import { validatePhoneNumber, formatPhoneForStorage } from '$lib/utils/phone.js';
56
import {
67
industries,
78
accountTypes,
@@ -44,13 +45,24 @@ export const actions = {
4445
return fail(400, { error: 'Account name is required' });
4546
}
4647

48+
// Validate phone number if provided
49+
let formattedPhone = null;
50+
const phone = formData.get('phone')?.toString();
51+
if (phone && phone.trim().length > 0) {
52+
const phoneValidation = validatePhoneNumber(phone.trim());
53+
if (!phoneValidation.isValid) {
54+
return fail(400, { error: phoneValidation.error || 'Please enter a valid phone number' });
55+
}
56+
formattedPhone = formatPhoneForStorage(phone.trim());
57+
}
58+
4759
// Extract all form fields
4860
const accountData = {
4961
name,
5062
type: formData.get('type')?.toString() || null,
5163
industry: formData.get('industry')?.toString() || null,
5264
website: formData.get('website')?.toString() || null,
53-
phone: formData.get('phone')?.toString() || null,
65+
phone: formattedPhone,
5466
street: formData.get('street')?.toString() || null,
5567
city: formData.get('city')?.toString() || null,
5668
state: formData.get('state')?.toString() || null,

src/routes/(app)/app/accounts/new/+page.svelte

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
<p class="text-sm font-medium {toastType === 'success' ? 'text-green-800 dark:text-green-200' : 'text-red-800 dark:text-red-200'}">{toastMessage}</p>
165165
</div>
166166
<button
167-
on:click={() => showToast = false}
167+
onclick={() => showToast = false}
168168
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'}">
169169
<X class="w-4 h-4" />
170170
</button>
@@ -241,7 +241,7 @@
241241
name="name"
242242
type="text"
243243
bind:value={formData.name}
244-
on:input={handleChange}
244+
oninput={handleChange}
245245
placeholder="Enter account name"
246246
required
247247
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' : ''}" />
@@ -259,7 +259,7 @@
259259
id="type"
260260
name="type"
261261
bind:value={formData.type}
262-
on:change={handleChange}
262+
onchange={handleChange}
263263
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">
264264
{#each data.data.accountTypes as [value, label]}
265265
<option value={value}>{label}</option>
@@ -276,7 +276,7 @@
276276
id="industry"
277277
name="industry"
278278
bind:value={formData.industry}
279-
on:change={handleChange}
279+
onchange={handleChange}
280280
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">
281281
{#each data.data.industries as [value, label]}
282282
<option value={value}>{label}</option>
@@ -293,7 +293,7 @@
293293
id="rating"
294294
name="rating"
295295
bind:value={formData.rating}
296-
on:change={handleChange}
296+
onchange={handleChange}
297297
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">
298298
{#each data.data.ratings as [value, label]}
299299
<option value={value}>{label}</option>
@@ -310,7 +310,7 @@
310310
id="accountOwnership"
311311
name="accountOwnership"
312312
bind:value={formData.accountOwnership}
313-
on:change={handleChange}
313+
onchange={handleChange}
314314
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">
315315
{#each data.data.accountOwnership as [value, label]}
316316
<option value={value}>{label}</option>
@@ -342,7 +342,7 @@
342342
name="website"
343343
type="url"
344344
bind:value={formData.website}
345-
on:input={handleChange}
345+
oninput={handleChange}
346346
placeholder="https://company.com"
347347
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' : ''}" />
348348
{#if errors.website}
@@ -361,7 +361,7 @@
361361
name="phone"
362362
type="tel"
363363
bind:value={formData.phone}
364-
on:input={handleChange}
364+
oninput={handleChange}
365365
placeholder="+1 (555) 123-4567"
366366
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' : ''}" />
367367
{#if errors.phone}
@@ -392,7 +392,7 @@
392392
name="street"
393393
type="text"
394394
bind:value={formData.street}
395-
on:input={handleChange}
395+
oninput={handleChange}
396396
placeholder="Street address"
397397
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" />
398398
</div>
@@ -404,7 +404,7 @@
404404
name="city"
405405
type="text"
406406
bind:value={formData.city}
407-
on:input={handleChange}
407+
oninput={handleChange}
408408
placeholder="City"
409409
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" />
410410
</div>
@@ -416,7 +416,7 @@
416416
name="state"
417417
type="text"
418418
bind:value={formData.state}
419-
on:input={handleChange}
419+
oninput={handleChange}
420420
placeholder="State"
421421
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" />
422422
</div>
@@ -428,7 +428,7 @@
428428
name="postalCode"
429429
type="text"
430430
bind:value={formData.postalCode}
431-
on:input={handleChange}
431+
oninput={handleChange}
432432
placeholder="Postal code"
433433
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" />
434434
</div>
@@ -439,7 +439,7 @@
439439
id="country"
440440
name="country"
441441
bind:value={formData.country}
442-
on:change={handleChange}
442+
onchange={handleChange}
443443
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">
444444
{#each data.data.countries as [value, label]}
445445
<option value={value}>{label}</option>
@@ -472,7 +472,7 @@
472472
type="number"
473473
min="0"
474474
bind:value={formData.numberOfEmployees}
475-
on:input={handleChange}
475+
oninput={handleChange}
476476
placeholder="100"
477477
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' : ''}" />
478478
{#if errors.numberOfEmployees}
@@ -493,7 +493,7 @@
493493
min="0"
494494
step="0.01"
495495
bind:value={formData.annualRevenue}
496-
on:input={handleChange}
496+
oninput={handleChange}
497497
placeholder="1000000"
498498
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' : ''}" />
499499
{#if errors.annualRevenue}
@@ -512,7 +512,7 @@
512512
name="tickerSymbol"
513513
type="text"
514514
bind:value={formData.tickerSymbol}
515-
on:input={handleChange}
515+
oninput={handleChange}
516516
placeholder="AAPL"
517517
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" />
518518
</div>
@@ -527,7 +527,7 @@
527527
name="sicCode"
528528
type="text"
529529
bind:value={formData.sicCode}
530-
on:input={handleChange}
530+
oninput={handleChange}
531531
placeholder="7372"
532532
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" />
533533
</div>
@@ -550,7 +550,7 @@
550550
id="description"
551551
name="description"
552552
bind:value={formData.description}
553-
on:input={handleChange}
553+
oninput={handleChange}
554554
placeholder="Additional notes about this account..."
555555
rows="4"
556556
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>
@@ -564,7 +564,7 @@
564564
<div class="flex justify-end gap-4">
565565
<button
566566
type="button"
567-
on:click={() => goto('/app/accounts')}
567+
onclick={() => goto('/app/accounts')}
568568
disabled={isSubmitting}
569569
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">
570570
<X class="w-4 h-4" />

0 commit comments

Comments
 (0)