From a97e8810bc22633a95e1f8491ed984855276a938 Mon Sep 17 00:00:00 2001 From: Ashwin Date: Fri, 1 Aug 2025 20:27:22 +0530 Subject: [PATCH 1/6] feat: enhance type safety and improve error handling in account-related routes and forms --- src/routes/(app)/app/accounts/+page.server.js | 8 +-- src/routes/(app)/app/accounts/+page.svelte | 26 +++++++-- .../app/accounts/[accountId]/+page.server.js | 11 ++-- .../app/accounts/[accountId]/+page.svelte | 58 ++++++++----------- .../accounts/[accountId]/edit/+page.server.js | 10 ++-- .../(app)/app/accounts/new/+page.server.js | 8 +-- .../(app)/app/accounts/new/+page.svelte | 52 +++++++++++++---- 7 files changed, 103 insertions(+), 70 deletions(-) diff --git a/src/routes/(app)/app/accounts/+page.server.js b/src/routes/(app)/app/accounts/+page.server.js index 27813d7..39dadf1 100644 --- a/src/routes/(app)/app/accounts/+page.server.js +++ b/src/routes/(app)/app/accounts/+page.server.js @@ -1,7 +1,7 @@ import { error } from '@sveltejs/kit'; import prisma from '$lib/prisma'; -export async function load({ locals, url, params }) { +export async function load({ locals, url }) { const org = locals.org; const page = parseInt(url.searchParams.get('page') || '1'); @@ -13,15 +13,15 @@ export async function load({ locals, url, params }) { try { // Build the where clause for filtering + /** @type {import('@prisma/client').Prisma.AccountWhereInput} */ const where = {organizationId: org.id}; // Add status filter const status = url.searchParams.get('status'); if (status === 'open') { - where.closedAt = null; - where.active = true; + where.isActive = true; } else if (status === 'closed') { - where.closedAt = { not: null }; + where.isActive = false; } // Fetch accounts with pagination diff --git a/src/routes/(app)/app/accounts/+page.svelte b/src/routes/(app)/app/accounts/+page.svelte index efbbd38..7bcf3ae 100644 --- a/src/routes/(app)/app/accounts/+page.svelte +++ b/src/routes/(app)/app/accounts/+page.svelte @@ -1,8 +1,7 @@ @@ -256,7 +268,7 @@

- {formatCurrency(opp.amount)} + {formatCurrency(opp.amount || 0)}

{opp.stage.replace('_', ' ')} diff --git a/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte b/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte index 80a3f45..2cda2c1 100644 --- a/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte +++ b/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte @@ -6,28 +6,29 @@ import { User, Mail, Phone, Building, MapPin, FileText, Star, Save, X, ArrowLeft } from '@lucide/svelte'; import { validatePhoneNumber } from '$lib/utils/phone.js'; - export let data; + /** @type {{ data: import('./$types').PageData }} */ + let { data } = $props(); - let contact = data.contact; + const { contact } = data; let account = data.account; let isPrimary = data.isPrimary; let role = data.role; - let firstName = contact.firstName; - let lastName = contact.lastName; - let email = contact.email || ''; - let phone = contact.phone || ''; - let title = contact.title || ''; - let department = contact.department || ''; - let street = contact.street || ''; - let city = contact.city || ''; - let state = contact.state || ''; - let postalCode = contact.postalCode || ''; - let country = contact.country || ''; - let description = contact.description || ''; - let submitting = false; - let errorMsg = ''; - let phoneError = ''; + let firstName = $state(contact?.firstName || ''); + let lastName = $state(contact?.lastName || ''); + let email = $state(contact?.email || ''); + let phone = $state(contact?.phone || ''); + let title = $state(contact?.title || ''); + let department = $state(contact?.department || ''); + let street = $state(contact?.street || ''); + let city = $state(contact?.city || ''); + let stateField = $state(contact?.state || ''); // Renamed to avoid conflict with Svelte's $state + let postalCode = $state(contact?.postalCode || ''); + let country = $state(contact?.country || ''); + let description = $state(contact?.description || ''); + let submitting = $state(false); + let errorMsg = $state(''); + let phoneError = $state(''); // Validate phone number on input function validatePhone() { @@ -44,6 +45,7 @@ } } + /** @param {Event} e */ async function handleSubmit(e) { e.preventDefault(); submitting = true; @@ -57,7 +59,7 @@ formData.append('department', department); formData.append('street', street); formData.append('city', city); - formData.append('state', state); + formData.append('state', stateField); formData.append('postalCode', postalCode); formData.append('country', country); formData.append('description', description); @@ -69,7 +71,7 @@ }); if (res.ok) { await invalidateAll(); - goto(`/app/contacts/${contact.id}`); + goto(`/app/contacts/${contact?.id}`); } else { const data = await res.json(); errorMsg = data?.message || 'Failed to update contact.'; @@ -84,7 +86,7 @@
@@ -367,7 +369,7 @@
{/if} - - {#if lead.annualRevenue} -
-
- - Annual Revenue -
-

- ${lead.annualRevenue.toLocaleString()} -

-
- {/if} -
@@ -491,19 +501,6 @@
- - {#if lead.address} -
-
- - Address -
-
-

{lead.address}

-
-
- {/if} - {#if lead.description}
@@ -684,14 +681,14 @@
Days Since Created - {Math.floor((new Date() - new Date(lead.createdAt)) / (1000 * 60 * 60 * 24))} + {Math.floor((new Date().getTime() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24))}
{#if lead.convertedAt}
Days to Convert - {Math.floor((new Date(lead.convertedAt) - new Date(lead.createdAt)) / (1000 * 60 * 60 * 24))} + {Math.floor((new Date(lead.convertedAt).getTime() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24))}
{/if} diff --git a/src/routes/(app)/app/leads/new/+page.svelte b/src/routes/(app)/app/leads/new/+page.svelte index f0df233..49761dd 100644 --- a/src/routes/(app)/app/leads/new/+page.svelte +++ b/src/routes/(app)/app/leads/new/+page.svelte @@ -35,6 +35,7 @@ /** * Object holding the form fields. + * @type {Record} */ let formData = { lead_title: '', @@ -73,18 +74,25 @@ /** * Object to store field errors. + * @type {Record} */ let errors = {}; /** * Handles changes to form inputs + * @param {Event} event */ function handleChange(event) { - const { name, value } = event.target; - formData[name] = value; - // Clear error when user starts typing - if (errors[name]) { - errors[name] = ''; + const target = event.target; + if (!target || !('name' in target && 'value' in target)) return; + const name = target.name; + const value = target.value; + if (typeof name === 'string' && typeof value === 'string') { + formData[name] = value; + // Clear error when user starts typing + if (errors[name]) { + errors[name] = ''; + } } } @@ -139,7 +147,7 @@ } // Validate probability range - if (formData.probability && (formData.probability < 0 || formData.probability > 100)) { + if (formData.probability && (Number(formData.probability) < 0 || Number(formData.probability) > 100)) { errors.probability = 'Probability must be between 0 and 100'; isValid = false; } @@ -190,6 +198,10 @@ errors = {}; } + /** + * @param {string} message + * @param {'success' | 'error'} type + */ function showNotification(message, type = 'success') { toastMessage = message; toastType = type; @@ -284,7 +296,10 @@ resetForm(); setTimeout(() => goto('/app/leads/open'), 1500); } else if (result.type === 'failure') { - showNotification(result.data?.error || 'Failed to create lead', 'error'); + const errorMessage = result.data && typeof result.data === 'object' && 'error' in result.data + ? String(result.data.error) + : 'Failed to create lead'; + showNotification(errorMessage, 'error'); } }; }} class="space-y-6"> diff --git a/src/routes/(app)/app/leads/open/+page.svelte b/src/routes/(app)/app/leads/open/+page.svelte index 67d7ec7..5361e02 100644 --- a/src/routes/(app)/app/leads/open/+page.svelte +++ b/src/routes/(app)/app/leads/open/+page.svelte @@ -77,11 +77,17 @@ ]; // Function to get the full name of a lead + /** + * @param {any} lead + */ function getFullName(lead) { return `${lead.firstName} ${lead.lastName}`.trim(); } // Function to map lead status to colors and icons + /** + * @param {string} status + */ function getStatusConfig(status) { switch (status) { case 'NEW': @@ -102,6 +108,9 @@ } // Function to get rating config + /** + * @param {string} rating + */ function getRatingConfig(rating) { switch (rating) { case 'Hot': @@ -116,6 +125,9 @@ } // Replace fixed date formatting with relative time + /** + * @param {string | Date | null | undefined} dateString + */ function formatDate(dateString) { if (!dateString) return '-'; return formatDistanceToNow(new Date(dateString), { addSuffix: true }); @@ -134,8 +146,12 @@ return matchesSearch && matchesStatus && matchesSource && matchesRating; }).sort((a, b) => { - const aValue = a[sortBy]; - const bValue = b[sortBy]; + const getFieldValue = (/** @type {any} */ obj, /** @type {string} */ field) => { + return obj[field]; + }; + + const aValue = getFieldValue(a, sortBy); + const bValue = getFieldValue(b, sortBy); if (sortOrder === 'asc') { return aValue > bValue ? 1 : -1; @@ -145,6 +161,9 @@ }); // Function to toggle sort order + /** + * @param {string} field + */ function toggleSort(field) { if (sortBy === field) { sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; @@ -320,7 +339,7 @@ {#each filteredLeads as lead, i} {@const statusConfig = getStatusConfig(lead.status)} - {@const ratingConfig = getRatingConfig(lead.rating)} + {@const ratingConfig = getRatingConfig(lead.rating || '')}
- {#snippet statusIcon(config)} + {#snippet statusIcon(/** @type {any} */ config)} {@const StatusIcon = config.icon} {/snippet} @@ -440,7 +459,7 @@
{#each filteredLeads as lead, i} {@const statusConfig = getStatusConfig(lead.status)} - {@const ratingConfig = getRatingConfig(lead.rating)} + {@const ratingConfig = getRatingConfig(lead.rating || '')}
- {#snippet statusIcon(config)} + {#snippet statusIcon(/** @type {any} */ config)} {@const StatusIcon = config.icon} {/snippet} diff --git a/src/routes/(app)/app/profile/+page.svelte b/src/routes/(app)/app/profile/+page.svelte index 5cf3ffd..53b5533 100644 --- a/src/routes/(app)/app/profile/+page.svelte +++ b/src/routes/(app)/app/profile/+page.svelte @@ -251,7 +251,7 @@ - -
-
-
💸
-
- -
-
-
- Paid - Due: 10 Mar 2025 -
-
-

INV-002

-

Beta LLC

-
-
- Design$800 -
+
+
{formatCurrency(Number(invoice.grandTotal))}
+
+ View + Edit
+ +
-
-
- Total - $800 -
-
- -
+ {/each} + + + {#if data.invoices.length === 0} +
+
📄
+

No invoices yet

+

Create your first invoice to get started

+ + Create Invoice +
-
-
-
- -
- + {/if}
diff --git a/src/routes/(app)/app/invoices/[invoiceId]/+page.server.js b/src/routes/(app)/app/invoices/[invoiceId]/+page.server.js new file mode 100644 index 0000000..6535ac4 --- /dev/null +++ b/src/routes/(app)/app/invoices/[invoiceId]/+page.server.js @@ -0,0 +1,63 @@ +import { error, redirect } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals }) { + if (!locals.user || !locals.org) { + throw redirect(302, '/login'); + } + + const invoice = await prisma.quote.findFirst({ + where: { + id: params.invoiceId, + organizationId: locals.org.id + }, + include: { + account: { + select: { + id: true, + name: true, + street: true, + city: true, + state: true, + postalCode: true, + country: true + } + }, + contact: { + select: { + firstName: true, + lastName: true, + email: true + } + }, + lineItems: { + include: { + product: { + select: { + name: true, + code: true + } + } + }, + orderBy: { + id: 'asc' + } + }, + preparedBy: { + select: { + name: true, + email: true + } + } + } + }); + + if (!invoice) { + throw error(404, 'Invoice not found'); + } + + return { + invoice + }; +}; diff --git a/src/routes/(app)/app/invoices/[invoiceId]/+page.svelte b/src/routes/(app)/app/invoices/[invoiceId]/+page.svelte index 345f84e..ca160d6 100644 --- a/src/routes/(app)/app/invoices/[invoiceId]/+page.svelte +++ b/src/routes/(app)/app/invoices/[invoiceId]/+page.svelte @@ -1,77 +1,161 @@ + +
-

INVOICE

-
#INV-2025-001
+

Invoice

+
{invoice.quoteNumber}
-
-
Acme Corporation
-
123 Main Street
New York, NY 10001
+
+
Prepared by:
+
{invoice.preparedBy.name}
+
{invoice.preparedBy.email}
-
+ +
-
Billed To:
-
Beta LLC
-
456 Market Ave
San Francisco, CA 94111
+
From:
+
{invoice.account.name}
+
+ {#if invoice.account.street} + {invoice.account.street}
+ {/if} + {#if invoice.account.city} + {invoice.account.city}{#if invoice.account.state}, {invoice.account.state}{/if} {invoice.account.postalCode}
+ {/if} + {#if invoice.account.country} + {invoice.account.country} + {/if} +
+
+
+
To:
+
+ {#if invoice.contact} + {invoice.contact.firstName} {invoice.contact.lastName} + {:else} + {invoice.account.name} + {/if} +
+
+ {#if invoice.contact && invoice.contact.email} + {invoice.contact.email} + {/if} +
- Status:Unpaid + Status: + + {invoice.status.toLowerCase()} +
- Invoice Date:2025-04-01 + Created:{new Date(invoice.createdAt).toLocaleDateString()}
- Due Date:2025-04-15 + Due Date:{invoice.expirationDate ? new Date(invoice.expirationDate).toLocaleDateString() : 'N/A'}
- - - - - - - - - - - - - - - - - - - - - - - -
DescriptionQuantityRateTotal
Consulting Services10$100$1,000
Hosting2$100$200
-
-
- Subtotal: - $1,200.00 + +
+ + + + + + + + + + + {#each invoice.lineItems as item} + + + + + + + {/each} + + + + + + + + + + + +
DescriptionQuantityRateTotal
{item.description || item.product?.name || 'N/A'}{item.quantity}{formatCurrency(Number(item.unitPrice))}{formatCurrency(Number(item.totalPrice))}
Subtotal:{formatCurrency(Number(invoice.subtotal))}
Total:{formatCurrency(Number(invoice.grandTotal))}
+
+ + {#if invoice.description} +
+
Notes:
+
+ {invoice.description} +
-
- Total: - $1,200.00 + {/if} + + -
-
Notes
-
Thank you for your business. Please make the payment by the due date.
-
-
- Edit -
- + + +
+
diff --git a/src/routes/(app)/app/invoices/[invoiceId]/edit/+page.server.js b/src/routes/(app)/app/invoices/[invoiceId]/edit/+page.server.js new file mode 100644 index 0000000..3689bde --- /dev/null +++ b/src/routes/(app)/app/invoices/[invoiceId]/edit/+page.server.js @@ -0,0 +1,127 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ params, locals }) { + if (!locals.user || !locals.org) { + throw redirect(302, '/login'); + } + + const invoice = await prisma.quote.findFirst({ + where: { + id: params.invoiceId, + organizationId: locals.org.id + }, + include: { + account: { + select: { + id: true, + name: true + } + }, + lineItems: { + include: { + product: { + select: { + id: true, + name: true, + code: true + } + } + }, + orderBy: { + id: 'asc' + } + } + } + }); + + if (!invoice) { + throw error(404, 'Invoice not found'); + } + + // Get accounts for the dropdown + const accounts = await prisma.account.findMany({ + where: { + organizationId: locals.org.id, + isActive: true, + isDeleted: false + }, + select: { + id: true, + name: true + }, + orderBy: { + name: 'asc' + } + }); + + return { + invoice, + accounts + }; +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, params, locals }) => { + if (!locals.user || !locals.org) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + + const accountId = String(formData.get('account_id') || ''); + const invoiceDate = String(formData.get('invoice_date') || ''); + const dueDate = String(formData.get('due_date') || ''); + const status = String(formData.get('status') || 'DRAFT'); + const notes = String(formData.get('notes') || ''); + + // Validation + if (!accountId || !invoiceDate || !dueDate) { + return fail(400, { + error: 'Account, invoice date, and due date are required' + }); + } + + try { + const invoice = await prisma.quote.findFirst({ + where: { + id: params.invoiceId, + organizationId: locals.org.id + } + }); + + if (!invoice) { + return fail(404, { error: 'Invoice not found' }); + } + + // Convert status for Quote model + const quoteStatus = status === 'DRAFT' ? 'DRAFT' : + status === 'SENT' ? 'PRESENTED' : + status === 'PAID' ? 'ACCEPTED' : 'DRAFT'; + + await prisma.quote.update({ + where: { + id: params.invoiceId + }, + data: { + accountId, + status: quoteStatus, + description: notes, + expirationDate: new Date(dueDate), + updatedAt: new Date() + } + }); + + throw redirect(303, `/app/invoices/${params.invoiceId}`); + } catch (err) { + if (err instanceof Response) throw err; // Re-throw redirects + + console.error('Error updating invoice:', err); + return fail(500, { + error: 'Failed to update invoice. Please try again.' + }); + } + } +}; diff --git a/src/routes/(app)/app/invoices/[invoiceId]/edit/+page.svelte b/src/routes/(app)/app/invoices/[invoiceId]/edit/+page.svelte index 87452a8..e69de29 100644 --- a/src/routes/(app)/app/invoices/[invoiceId]/edit/+page.svelte +++ b/src/routes/(app)/app/invoices/[invoiceId]/edit/+page.svelte @@ -1,85 +0,0 @@ - - -
-
-
-
-

Edit Invoice

-

Invoice #INV-001

-
-
- Unpaid -
Due: 2025-04-15
-
-
-
-
-
-
Account
-
Acme Corp
-
-
-
Invoice Date
-
2025-04-01
-
-
-
Due Date
-
2025-04-15
-
-
-
Status
-
Unpaid
-
-
-
-
-
Line Items
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DescriptionQuantityRateTotal
Consulting Services10$100$1,000×
Hosting2$100$200×
Total$1,200
-
-
-
-
Notes
-
Thank you for your business.
-
-
- -
-
-
-
diff --git a/src/routes/(app)/app/invoices/new/+page.server.js b/src/routes/(app)/app/invoices/new/+page.server.js new file mode 100644 index 0000000..e58025c --- /dev/null +++ b/src/routes/(app)/app/invoices/new/+page.server.js @@ -0,0 +1,87 @@ +import { fail, redirect } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; + +/** @type {import('./$types').PageServerLoad} */ +export async function load({ locals }) { + if (!locals.user || !locals.org) { + throw redirect(302, '/login'); + } + + // Get accounts for the dropdown + const accounts = await prisma.account.findMany({ + where: { + organizationId: locals.org.id, + isActive: true, + isDeleted: false + }, + select: { + id: true, + name: true + }, + orderBy: { + name: 'asc' + } + }); + + return { + accounts + }; +} + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ request, locals }) => { + if (!locals.user || !locals.org) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + + const invoiceNumber = String(formData.get('invoice_number') || ''); + const accountId = String(formData.get('account_id') || ''); + const invoiceDate = String(formData.get('invoice_date') || ''); + const dueDate = String(formData.get('due_date') || ''); + const status = String(formData.get('status') || 'DRAFT'); + const notes = String(formData.get('notes') || ''); + + // Validation + if (!invoiceNumber || !accountId || !invoiceDate || !dueDate) { + return fail(400, { + error: 'Invoice number, account, invoice date, and due date are required' + }); + } + + try { + // For now, we'll create a Quote since that's what exists in the schema + // In a real implementation, you might want to add an Invoice model + // or extend the Quote model to handle invoices + + // Generate unique quote number (since we're using Quote model) + const quoteNumber = `INV-${Date.now()}`; + + const quote = await prisma.quote.create({ + data: { + quoteNumber, + name: `Invoice ${invoiceNumber}`, + status: status === 'DRAFT' ? 'DRAFT' : + status === 'SENT' ? 'PRESENTED' : + status === 'PAID' ? 'ACCEPTED' : 'DRAFT', + description: notes, + expirationDate: new Date(dueDate), + subtotal: 0, // Will be updated when line items are added + grandTotal: 0, + preparedById: locals.user.id, + accountId: accountId, + organizationId: locals.org.id + } + }); + + throw redirect(303, `/app/invoices/${quote.id}`); + } catch (error) { + console.error('Error creating invoice:', error); + return fail(500, { + error: 'Failed to create invoice. Please try again.' + }); + } + } +}; diff --git a/src/routes/(app)/app/invoices/new/+page.svelte b/src/routes/(app)/app/invoices/new/+page.svelte index e7a58e3..40a93a8 100644 --- a/src/routes/(app)/app/invoices/new/+page.svelte +++ b/src/routes/(app)/app/invoices/new/+page.svelte @@ -1,6 +1,66 @@ + +
-
+

New Invoice

@@ -10,85 +70,193 @@ DRAFT
-
-
- -
INV-0001
-
-
- -
Acme Corporation
-
-
- -
2025-04-01
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
- -
2025-04-15
+
+

Line Items

+ +
+ +
+ + + + + + + + + + + + {#each lineItems as item, index (index)} + + + + + + + + {/each} + + + + + + + + + + + + + +
DescriptionQuantityRateTotal
+ { + const target = e.target; + if (target instanceof HTMLInputElement) { + updateLineItem(index, 'description', target.value); + } + }} + class="w-full border-none bg-transparent focus:ring-2 focus:ring-blue-500 rounded px-2 py-1" + placeholder="Description" /> + + { + const target = e.target; + if (target instanceof HTMLInputElement) { + updateLineItem(index, 'quantity', Number(target.value)); + } + }} + class="w-20 border-none bg-transparent focus:ring-2 focus:ring-blue-500 rounded px-2 py-1 text-right" + min="1" /> + + { + const target = e.target; + if (target instanceof HTMLInputElement) { + updateLineItem(index, 'rate', Number(target.value)); + } + }} + class="w-24 border-none bg-transparent focus:ring-2 focus:ring-blue-500 rounded px-2 py-1 text-right" + min="0" + step="0.01" /> + + ${item.total.toFixed(2)} + + +
Subtotal:${subtotal.toFixed(2)}
Total:${grandTotal.toFixed(2)}
+
+ +
- -
Draft
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DescriptionQuantityRateTotal
Consulting Services10$100$1,000
Hosting2$100$200
Total$1,200
+ +
-
- -
-
-
-
-
- Subtotal: - $1,200.00 -
-
- Total: - $1,200.00 -
+ + +
+ +
-
-
- -
Thank you for your business!
-
-
- - -
+
From 9bfd97058effdb1c7e0babb446e6311b18a76884 Mon Sep 17 00:00:00 2001 From: Ashwin Date: Fri, 1 Aug 2025 21:53:45 +0530 Subject: [PATCH 5/6] feat: Improve accessibility by adding 'for' attributes to labels and enhancing UI elements in contact details --- .../(admin)/admin/contacts/+page.svelte | 1 + src/routes/(app)/app/contacts/+page.svelte | 37 ++++++++++++++++++- .../app/contacts/[contactId]/+page.svelte | 16 ++++---- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/routes/(admin)/admin/contacts/+page.svelte b/src/routes/(admin)/admin/contacts/+page.svelte index 87c4428..3461051 100644 --- a/src/routes/(admin)/admin/contacts/+page.svelte +++ b/src/routes/(admin)/admin/contacts/+page.svelte @@ -2,6 +2,7 @@ /** @type {{ data: import('./$types').PageData }} */ let { data } = $props(); + /** @param {string | Date} dateString */ const formatDate = (dateString) => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', diff --git a/src/routes/(app)/app/contacts/+page.svelte b/src/routes/(app)/app/contacts/+page.svelte index 0c31033..0430d61 100644 --- a/src/routes/(app)/app/contacts/+page.svelte +++ b/src/routes/(app)/app/contacts/+page.svelte @@ -149,10 +149,11 @@
-
+ {/each} diff --git a/src/routes/(app)/app/contacts/[contactId]/+page.svelte b/src/routes/(app)/app/contacts/[contactId]/+page.svelte index 427bdc5..e19c454 100644 --- a/src/routes/(app)/app/contacts/[contactId]/+page.svelte +++ b/src/routes/(app)/app/contacts/[contactId]/+page.svelte @@ -123,7 +123,7 @@
- + Title

{contact.title || 'N/A'}

- + Owner

{contact.owner?.name || 'N/A'}

- + Created

{formatDate(contact.createdAt)} @@ -169,7 +169,7 @@

{#if contact.description}
- + Description

{contact.description}

{/if} @@ -193,7 +193,7 @@ {relationship.account.name}
{#if relationship.isPrimary} - + Primary From 590fe1cbb31fcbb30cbc895baa45d7c7a65af3e8 Mon Sep 17 00:00:00 2001 From: Ashwin Date: Fri, 1 Aug 2025 22:14:57 +0530 Subject: [PATCH 6/6] feat: Enhance type safety with JSDoc annotations and improve error handling across various components and routes --- src/lib/newsletter.js | 6 +-- src/lib/stores/auth.js | 3 ++ src/routes/(admin)/+layout.svelte | 7 ++- src/routes/(admin)/admin/+page.svelte | 2 +- src/routes/(admin)/admin/blogs/+page.svelte | 2 +- .../(admin)/admin/blogs/[id]/+page.svelte | 40 +++++++------- .../admin/blogs/[id]/edit/+page.server.js | 54 ++++++++++++++----- .../admin/blogs/[id]/edit/+page.svelte | 19 +++---- .../(admin)/admin/blogs/new/+page.server.js | 14 ++--- .../(admin)/admin/blogs/new/+page.svelte | 6 +-- .../(admin)/admin/newsletter/+page.svelte | 6 +-- src/routes/(app)/Sidebar.svelte | 19 +++---- src/routes/(app)/app/+page.svelte | 10 ++-- .../app/accounts/[accountId]/+page.svelte | 37 +++++++------ 14 files changed, 133 insertions(+), 92 deletions(-) diff --git a/src/lib/newsletter.js b/src/lib/newsletter.js index 38e47c0..6dd850d 100644 --- a/src/lib/newsletter.js +++ b/src/lib/newsletter.js @@ -1,7 +1,7 @@ // Newsletter utility functions for email confirmation and management /** - * Generate unsubscribe link for newsletter + * Generate unsubscribe link * @param {string} token - Confirmation token * @param {string} baseUrl - Base URL of the application * @returns {string} Unsubscribe URL @@ -95,14 +95,14 @@ export function generateWelcomeEmail(email, unsubscribeLink) { /** * Generate newsletter template for regular updates - * @param {object} content - Newsletter content + * @param {any} content - Newsletter content * @param {string} unsubscribeLink - Unsubscribe link * @returns {object} Newsletter template with subject and body */ export function generateNewsletterTemplate(content, unsubscribeLink) { const { subject, headline, articles = [], ctaText = 'Learn More', ctaLink = 'https://bottlecrm.io' } = content; - const articlesHtml = articles.map(article => ` + const articlesHtml = articles.map(/** @param {any} article */ article => `

${article.title}

${article.excerpt}

diff --git a/src/lib/stores/auth.js b/src/lib/stores/auth.js index ca27bf5..5145737 100644 --- a/src/lib/stores/auth.js +++ b/src/lib/stores/auth.js @@ -7,6 +7,9 @@ export const auth = writable({ }); // Helper to get the current session user from event.locals (SvelteKit convention) +/** + * @param {any} event + */ export function getSessionUser(event) { // If you use event.locals.user for authentication, return it // You can adjust this logic if your user is stored differently diff --git a/src/routes/(admin)/+layout.svelte b/src/routes/(admin)/+layout.svelte index 8658f13..741d59d 100644 --- a/src/routes/(admin)/+layout.svelte +++ b/src/routes/(admin)/+layout.svelte @@ -2,11 +2,16 @@ import '../../app.css' import { Menu, Bell, User, Search, FileText, Settings, ChartBar, Home, X, LogOut } from '@lucide/svelte'; - /** @type {{ data: import('./admin/$types').LayoutData, children: import('svelte').Snippet }} */ + /** @type {{ data?: any, children: import('svelte').Snippet }} */ let { data, children } = $props(); let mobileMenuOpen = $state(false); + const handleLogout = () => { + // Perform logout action - you might want to redirect to logout endpoint + window.location.href = '/logout'; + }; +
diff --git a/src/routes/(admin)/admin/+page.svelte b/src/routes/(admin)/admin/+page.svelte index 75d078e..ccf05e8 100644 --- a/src/routes/(admin)/admin/+page.svelte +++ b/src/routes/(admin)/admin/+page.svelte @@ -17,7 +17,7 @@ const { metrics } = data; // Format numbers with commas - const formatNumber = (num) => { + const formatNumber = (/** @type {any} */ num) => { return new Intl.NumberFormat('en-US').format(num); }; diff --git a/src/routes/(admin)/admin/blogs/+page.svelte b/src/routes/(admin)/admin/blogs/+page.svelte index 84cdae3..1ffac95 100644 --- a/src/routes/(admin)/admin/blogs/+page.svelte +++ b/src/routes/(admin)/admin/blogs/+page.svelte @@ -24,7 +24,7 @@ {#each data.blogs as blog} {blog.title} - Edit - {blog.category} + N/A {#if blog.draft} Draft diff --git a/src/routes/(admin)/admin/blogs/[id]/+page.svelte b/src/routes/(admin)/admin/blogs/[id]/+page.svelte index 3cbdfda..46ecdc2 100644 --- a/src/routes/(admin)/admin/blogs/[id]/+page.svelte +++ b/src/routes/(admin)/admin/blogs/[id]/+page.svelte @@ -17,10 +17,10 @@
@@ -35,15 +35,12 @@

- {data.blog.title} + {data.blog?.title || 'Untitled'}

@@ -52,7 +49,7 @@
- {#each data.blog.contentBlocks as block} + {#each data.blog?.contentBlocks || [] as block}
{#if block.type == "MARKDOWN"}
@@ -118,8 +115,8 @@

Blog Details

- Category: - {data.blog.category} + Type: + Blog Post
Status: @@ -134,14 +131,14 @@
{ const form = await request.formData(); + const type = form.get('type')?.toString(); + const content = form.get('content')?.toString(); + const displayOrder = form.get('displayOrder')?.toString(); + + if (!type || !content || !displayOrder) { + return { success: false, error: 'Missing required fields' }; + } + await prisma.blogContentBlock.create({ data: { blogId: params.id, - type: form.get('type'), - content: form.get('content'), - displayOrder: Number(form.get('displayOrder')), + type: /** @type {import('@prisma/client').ContentBlockType} */ (type), + content: content, + displayOrder: Number(displayOrder), draft: form.get('draft') === 'on' } }); @@ -34,11 +42,19 @@ export const actions = { }, 'edit-block': async ({ request }) => { const form = await request.formData(); + const id = form.get('id')?.toString(); + const type = form.get('type')?.toString(); + const content = form.get('content')?.toString(); + + if (!id || !type || !content) { + return { success: false, error: 'Missing required fields' }; + } + await prisma.blogContentBlock.update({ - where: { id: form.get('id') }, + where: { id: id }, data: { - type: form.get('type'), - content: form.get('content'), + type: /** @type {import('@prisma/client').ContentBlockType} */ (type), + content: content, draft: form.get('draft') === 'on' } }); @@ -46,19 +62,25 @@ export const actions = { }, 'delete-block': async ({ request }) => { const form = await request.formData(); + const id = form.get('id')?.toString(); + + if (!id) { + return { success: false, error: 'Missing block ID' }; + } + await prisma.blogContentBlock.delete({ - where: { id: form.get('id') } + where: { id: id } }); return { success: true }; }, 'update-blog': async ({ request, params }) => { const form = await request.formData(); const data = { - title: form.get('title'), - seoTitle: form.get('seoTitle'), - seoDescription: form.get('seoDescription'), - excerpt: form.get('excerpt'), - slug: form.get('slug'), + title: form.get('title')?.toString() || '', + seoTitle: form.get('seoTitle')?.toString() || '', + seoDescription: form.get('seoDescription')?.toString() || '', + excerpt: form.get('excerpt')?.toString() || '', + slug: form.get('slug')?.toString() || '', draft: form.get('draft') === 'on' }; await prisma.blogPost.update({ @@ -70,7 +92,13 @@ export const actions = { , 'reorder-blocks': async ({ request, params }) => { const form = await request.formData(); - const order = JSON.parse(form.get('order')); + const orderStr = form.get('order')?.toString(); + + if (!orderStr) { + return { success: false, error: 'Missing order data' }; + } + + const order = JSON.parse(orderStr); for (const { id, displayOrder } of order) { await prisma.blogContentBlock.update({ where: { id }, diff --git a/src/routes/(admin)/admin/blogs/[id]/edit/+page.svelte b/src/routes/(admin)/admin/blogs/[id]/edit/+page.svelte index 444bd4d..d25ab8f 100644 --- a/src/routes/(admin)/admin/blogs/[id]/edit/+page.svelte +++ b/src/routes/(admin)/admin/blogs/[id]/edit/+page.svelte @@ -2,17 +2,16 @@ import { dndzone } from "svelte-dnd-action"; /** @type {{ data: import('./$types').PageData }} */ export let data; - export let form; /** @type {any} */ - let blog = data.blog; + let blog = /** @type {any} */ (data)?.blog || {}; /** @type {any[]} */ - let contentBlocks = blog.contentBlocks + let contentBlocks = blog?.contentBlocks || [] // Drag and drop handler for reordering content blocks - async function handleReorder({ detail }) { + async function handleReorder(/** @type {any} */ { detail }) { // detail.items is the new order of contentBlocks // Reorder contentBlocks array to match the new order from dndzone - contentBlocks = detail.items.map((item, idx) => ({ + contentBlocks = detail.items.map((/** @type {any} */ item, /** @type {any} */ idx) => ({ ...item, displayOrder: idx + 1 })); @@ -29,6 +28,7 @@ let message = ""; // For editing/adding content blocks + /** @type {any} */ let editingBlockId = null; let newBlock = { type: "MARKDOWN", @@ -37,14 +37,14 @@ draft: false, }; - function startEditBlock(block) { + function startEditBlock(/** @type {any} */ block) { editingBlockId = block.id; /** @type {any} */ (block)._editContent = block.content; /** @type {any} */ (block)._editType = block.type; /** @type {any} */ (block)._editDraft = block.draft; } - function cancelEditBlock(block) { + function cancelEditBlock(/** @type {any} */ block) { editingBlockId = null; delete block._editContent; delete block._editType; @@ -61,12 +61,13 @@ .replace(/\s+/g, '-') .replace(/[^\w-]+/g, ''); } - let editable_title = form?.data?.title ?? blog.title; - let slug = form?.data?.slug ?? blog.slug; + let editable_title = blog?.title || ''; + let slug = blog?.slug || ''; // Initialize previous_editable_title_for_slug_generation to undefined // so we can detect the first run of the reactive block. + /** @type {any} */ let previous_editable_title_for_slug_generation = undefined; $: { diff --git a/src/routes/(admin)/admin/blogs/new/+page.server.js b/src/routes/(admin)/admin/blogs/new/+page.server.js index 9683932..3536c28 100644 --- a/src/routes/(admin)/admin/blogs/new/+page.server.js +++ b/src/routes/(admin)/admin/blogs/new/+page.server.js @@ -10,9 +10,9 @@ export async function load() { export const actions = { default: async ({ request }) => { const data = await request.formData(); - const title = data.get('title'); - const excerpt = data.get('excerpt'); - const slug = data.get('slug'); + const title = data.get('title')?.toString(); + const excerpt = data.get('excerpt')?.toString(); + const slug = data.get('slug')?.toString(); if (!title || !excerpt || !slug) { return { error: 'All fields are required' }; @@ -20,9 +20,9 @@ export const actions = { try { await prisma.blogPost.create({ data: { - title, - excerpt, - slug, + title: title, + excerpt: excerpt, + slug: slug, seoTitle:"", seoDescription: "", draft: true @@ -30,7 +30,7 @@ export const actions = { }); return { success: true }; } catch (e) { - return { error: e?.message || 'Error creating blog' }; + return { error: /** @type {any} */ (e)?.message || 'Error creating blog' }; } } }; \ No newline at end of file diff --git a/src/routes/(admin)/admin/blogs/new/+page.svelte b/src/routes/(admin)/admin/blogs/new/+page.svelte index c17a7e0..a2f8ea5 100644 --- a/src/routes/(admin)/admin/blogs/new/+page.svelte +++ b/src/routes/(admin)/admin/blogs/new/+page.svelte @@ -4,10 +4,10 @@ /** @type {import('./$types').PageProps} */ let { form } = $props(); - let title = $state(form?.data?.title ?? ''); - let excerpt = $state(form?.data?.excerpt ?? ''); + let title = $state(''); + let excerpt = $state(''); - function make_slug(title) { + function make_slug(/** @type {any} */ title) { return title .toLowerCase() .replace(/\s+/g, '-') diff --git a/src/routes/(admin)/admin/newsletter/+page.svelte b/src/routes/(admin)/admin/newsletter/+page.svelte index c52ed77..eee1ac1 100644 --- a/src/routes/(admin)/admin/newsletter/+page.svelte +++ b/src/routes/(admin)/admin/newsletter/+page.svelte @@ -6,7 +6,7 @@ export let data; // Format date helper - function formatDate(dateString) { + function formatDate(/** @type {any} */ dateString) { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', @@ -17,14 +17,14 @@ } // Get status badge class - function getStatusClass(isActive, isConfirmed) { + function getStatusClass(/** @type {any} */ isActive, /** @type {any} */ isConfirmed) { if (!isActive) return 'bg-red-100 text-red-800'; if (isConfirmed) return 'bg-green-100 text-green-800'; return 'bg-yellow-100 text-yellow-800'; } // Get status text - function getStatusText(isActive, isConfirmed) { + function getStatusText(/** @type {any} */ isActive, /** @type {any} */ isConfirmed) { if (!isActive) return 'Unsubscribed'; if (isConfirmed) return 'Active'; return 'Pending'; diff --git a/src/routes/(app)/Sidebar.svelte b/src/routes/(app)/Sidebar.svelte index 96687b7..9b33043 100644 --- a/src/routes/(app)/Sidebar.svelte +++ b/src/routes/(app)/Sidebar.svelte @@ -47,7 +47,7 @@ userDropdownOpen = !userDropdownOpen; }; - const handleSettingsLinkClick = (event, href) => { + const handleSettingsLinkClick = (/** @type {any} */ event, /** @type {any} */ href) => { event.preventDefault(); event.stopPropagation(); @@ -58,12 +58,12 @@ window.location.href = href; }; - const handleDropdownClick = (event) => { + const handleDropdownClick = (/** @type {any} */ event) => { // Prevent clicks inside dropdown from bubbling up event.stopPropagation(); }; - const handleClickOutside = (event) => { + const handleClickOutside = (/** @type {any} */ event) => { if (userDropdownOpen && dropdownRef && !dropdownRef.contains(event.target)) { userDropdownOpen = false; } @@ -80,9 +80,10 @@ }); let mainSidebarUrl = $derived($page.url.pathname); + /** @type {{ [key: string]: boolean }} */ let openDropdowns = $state({}); - const toggleDropdown = (key) => { + const toggleDropdown = (/** @type {any} */ key) => { openDropdowns[key] = !openDropdowns[key]; }; @@ -202,7 +203,7 @@ : 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800' }`} > - {@render item.icon({ class: 'w-5 h-5' })} + {item.label} {:else if item.type === 'dropdown'} @@ -213,13 +214,13 @@ onclick={() => toggleDropdown(item.key)} >
- {@render item.icon({ class: 'w-5 h-5' })} + {item.label}
- + - {#if openDropdowns[item.key]} + {#if item.key && openDropdowns[item.key] && item.children}
{#each item.children as child} - {@render child.icon({ class: 'w-4 h-4' })} + {child.label} {/each} diff --git a/src/routes/(app)/app/+page.svelte b/src/routes/(app)/app/+page.svelte index af3def3..8e4ecd0 100644 --- a/src/routes/(app)/app/+page.svelte +++ b/src/routes/(app)/app/+page.svelte @@ -19,22 +19,22 @@ $: metrics = data.metrics || {}; $: recentData = data.recentData || {}; - function formatCurrency(amount) { + function formatCurrency(/** @type {any} */ amount) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); } - function formatDate(date) { + function formatDate(/** @type {any} */ date) { return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } - function getStatusColor(status) { - const colors = { + function getStatusColor(/** @type {any} */ status) { + const colors = /** @type {{ [key: string]: string }} */ ({ 'NEW': 'bg-blue-100 text-blue-800', 'PENDING': 'bg-yellow-100 text-yellow-800', 'CONTACTED': 'bg-green-100 text-green-800', @@ -42,7 +42,7 @@ 'High': 'bg-red-100 text-red-800', 'Normal': 'bg-blue-100 text-blue-800', 'Low': 'bg-gray-100 text-gray-800' - }; + }); return colors[status] || 'bg-gray-100 text-gray-800'; } diff --git a/src/routes/(app)/app/accounts/[accountId]/+page.svelte b/src/routes/(app)/app/accounts/[accountId]/+page.svelte index 93360ab..a07234e 100644 --- a/src/routes/(app)/app/accounts/[accountId]/+page.svelte +++ b/src/routes/(app)/app/accounts/[accountId]/+page.svelte @@ -210,15 +210,15 @@