-
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'}
-
-
Subtotal:
-
$1,200.00
+
+
+
+
+
+ Description |
+ Quantity |
+ Rate |
+ Total |
+
+
+
+ {#each invoice.lineItems as item}
+
+ {item.description || item.product?.name || 'N/A'} |
+ {item.quantity} |
+ {formatCurrency(Number(item.unitPrice))} |
+ {formatCurrency(Number(item.totalPrice))} |
+
+ {/each}
+
+
+
+ Subtotal: |
+ {formatCurrency(Number(invoice.subtotal))} |
+
+
+
+
+
+
+
+
+
+ {#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.
-
-
-
+
+
+
+
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 @@
-
-
-
-
-
-
-
-
Unpaid
-
Due: 2025-04-15
-
-
-
-
-
-
-
Invoice Date
-
2025-04-01
-
-
-
Due Date
-
2025-04-15
-
-
-
-
-
-
Line Items
-
-
-
-
-
-
- Description |
- Quantity |
- Rate |
- Total |
- |
-
-
-
-
- Consulting Services |
- 10 |
- $100 |
- $1,000 |
- × |
-
-
- Hosting |
- 2 |
- $100 |
- $200 |
- × |
-
-
-
-
- Total |
-
- |
-
-
-
-
-
-
-
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 @@
+
+
-
+
@@ -10,85 +70,193 @@
DRAFT
-
-
-
-
INV-0001
-
-
-
-
Acme Corporation
-
-
-
-
2025-04-01
+
+
-
-
-
-
-
-
- Description |
- Quantity |
- Rate |
- Total |
-
-
-
-
- Consulting Services |
- 10 |
- $100 |
- $1,000 |
-
-
- Hosting |
- 2 |
- $100 |
- $200 |
-
-
-
-
- Total |
-
-
-
-
+
+
-
-
-
-
-
-
-
- Subtotal:
- $1,200.00
-
-
- Total:
- $1,200.00
-
+
+
+
+
+
-
-
-
-
Thank you for your business!
-
-
-
-
-
+
diff --git a/src/routes/(app)/app/leads/[lead_id]/+page.server.js b/src/routes/(app)/app/leads/[lead_id]/+page.server.js
index 795a4d5..c76aedb 100644
--- a/src/routes/(app)/app/leads/[lead_id]/+page.server.js
+++ b/src/routes/(app)/app/leads/[lead_id]/+page.server.js
@@ -239,7 +239,7 @@ export const actions = {
} catch (err) {
console.error('Error adding comment:', err instanceof Error ? err.message : String(err));
if (err instanceof z.ZodError) {
- return fail(400, { status: 'error', message: err.errors[0].message });
+ return fail(400, { status: 'error', message: err.issues[0].message });
}
return fail(500, { status: 'error', message: 'Failed to add comment' });
}
diff --git a/src/routes/(app)/app/leads/[lead_id]/+page.svelte b/src/routes/(app)/app/leads/[lead_id]/+page.svelte
index 6869213..d5c9cd4 100644
--- a/src/routes/(app)/app/leads/[lead_id]/+page.svelte
+++ b/src/routes/(app)/app/leads/[lead_id]/+page.svelte
@@ -51,11 +51,17 @@
let showConfirmModal = false;
// Function to get the full name of a lead
+ /**
+ * @param {any} lead
+ */
function getFullName(lead) {
return `${lead.firstName} ${lead.lastName}`.trim();
}
// Function to format date
+ /**
+ * @param {string | Date | null | undefined} dateString
+ */
function formatDate(dateString) {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleDateString('en-US', {
@@ -68,6 +74,9 @@
}
// Function to format date (short)
+ /**
+ * @param {string | Date | null | undefined} dateString
+ */
function formatDateShort(dateString) {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleDateString('en-US', {
@@ -78,6 +87,9 @@
}
// Function to map lead status to colors
+ /**
+ * @param {string} status
+ */
function getStatusColor(status) {
switch (status) {
case 'NEW':
@@ -98,12 +110,18 @@
}
// Function to get lead source display name
+ /**
+ * @param {string | null | undefined} source
+ */
function getLeadSourceDisplay(source) {
if (!source) return 'Unknown';
- return source.replace('_', ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase());
+ return source.replace('_', ' ').toLowerCase().replace(/\b\w/g, (/** @type {string} */ l) => l.toUpperCase());
}
// Function to get initials for avatar
+ /**
+ * @param {any} lead
+ */
function getInitials(lead) {
const first = lead.firstName?.[0] || '';
const last = lead.lastName?.[0] || '';
@@ -144,12 +162,17 @@
function confirmConversion() {
showConfirmModal = false;
// Submit the form programmatically
- document.getElementById('convertForm').requestSubmit();
+ const form = document.getElementById('convertForm');
+ if (form) {
+ // Use dispatchEvent as a cross-browser solution
+ const event = new Event('submit', { cancelable: true, bubbles: true });
+ form.dispatchEvent(event);
+ }
}
const enhanceConvertForm = () => {
isConverting = true;
- return async ({ update }) => {
+ return async (/** @type {{ update: any }} */ { update }) => {
await update({ reset: false });
// Note: If conversion is successful, the server will redirect automatically
// This will only execute if there's an error
@@ -159,7 +182,7 @@
const enhanceCommentForm = () => {
isSubmittingComment = true;
- return async ({ update }) => {
+ return async (/** @type {{ update: any }} */ { update }) => {
await update({ reset: false });
// Reset the loading state after update
isSubmittingComment = false;
@@ -455,19 +478,6 @@
{/if}
-
- {#if lead.annualRevenue}
-
-
-
- Annual Revenue
-
-
- ${lead.annualRevenue.toLocaleString()}
-
-
- {/if}
-
-
- {#if 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 @@
|