diff --git a/package.json b/package.json index e29cda1..8dfafd3 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@prisma/client": "6.5.0", "axios": "^1.8.4", "date-fns": "^4.1.0", - "flowbite-svelte-blocks": "^1.1.4", + "flowbite-svelte-blocks": "^2.0.0", "flowbite-svelte-icons": "^2.1.1", "svelte-fa": "^4.0.3", "uuid": "^11.1.0" diff --git a/prisma/migrations/20250418063418_/migration.sql b/prisma/migrations/20250418063418_/migration.sql new file mode 100644 index 0000000..a8cd0d7 --- /dev/null +++ b/prisma/migrations/20250418063418_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Case" ADD COLUMN "dueDate" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7f78a34..bf6367a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -352,6 +352,7 @@ model Case { origin String? // Email, Web, Phone type String? // Problem, Feature Request, Question reason String? // Example: "User didn't attend training, Complex functionality" + dueDate DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt closedAt DateTime? diff --git a/src/routes/(app)/Sidebar.svelte b/src/routes/(app)/Sidebar.svelte index c1818c8..e3e40b7 100644 --- a/src/routes/(app)/Sidebar.svelte +++ b/src/routes/(app)/Sidebar.svelte @@ -36,80 +36,6 @@ activeMainSidebar = navigation.to?.url.pathname ?? ''; }); - let menu = [ - { name: 'Dashboard', icon: 'ChartPieOutline', href: '/app/dashboard' }, - { name: 'Leads', icon: 'FunnelOutline', href: '/app/leads' }, - { name: 'Contacts', icon: 'UserOutline', href: '/app/contacts' }, - { - name: 'Accounts', - icon: 'BuildingOfficeOutline', - children: { - 'All Accounts': '/app/accounts', - 'New Account': '/app/accounts/new', - 'Account Opportunities': '/app/accounts/opportunities', - 'Deleted/Archived Accounts': '/app/accounts/deleted' - } - }, - { - name: 'Opportunities', - icon: 'CurrencyDollarOutline', - children: { - 'All Opportunities': '/app/opportunities', - 'New Opportunity': '/app/opportunities/new' - } - }, - { - name: 'Cases', - icon: 'LifebuoyOutline', - children: { - 'All Cases': '/app/cases', - 'New Case': '/app/cases/new' - } - }, - { - name: 'Tasks', - icon: 'ClipboardOutline', - children: { - 'All Tasks': '/app/tasks', - 'New Task': '/app/tasks/new', - Calendar: '/app/tasks/calendar' - } - }, - { - name: 'Invoices', - icon: 'ReceiptOutline', - children: { - 'All Invoices': '/app/invoices', - 'Create Invoice': '/app/invoices/new' - } - }, - { - name: 'Reports', - icon: 'ChartBarOutline', - children: { - 'Sales Reports': '/app/reports/sales', - 'Case Stats': '/app/reports/cases', - 'Invoice Trends': '/app/reports/invoices' - } - }, - { - name: 'Settings', - icon: 'CogOutline', - children: { - 'User Management': '/app/settings/users', - 'Custom Fields': '/app/settings/fields', - Integrations: '/app/settings/integrations' - } - } - ]; - let links = [ - { - label: 'Support', - href: 'https://discord.gg/', - icon: LifeSaverSolid - } - ]; - let dropdowns = Object.fromEntries(Object.keys(menu).map((x) => [x, false])); - - - - - - - - - - {#each links as { label, href, icon } (label)} - - - - {/each} - + diff --git a/src/routes/(app)/app/accounts/+page.svelte b/src/routes/(app)/app/accounts/+page.svelte index 36b2883..0a99c1f 100644 --- a/src/routes/(app)/app/accounts/+page.svelte +++ b/src/routes/(app)/app/accounts/+page.svelte @@ -66,6 +66,7 @@ /> + +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ {#if errorMsg} +
{errorMsg}
+ {/if} + + +
+ +
+ diff --git a/src/routes/(app)/app/cases/new/+page.server.js b/src/routes/(app)/app/cases/new/+page.server.js new file mode 100644 index 0000000..aa1ff2d --- /dev/null +++ b/src/routes/(app)/app/cases/new/+page.server.js @@ -0,0 +1,35 @@ +import prisma from '$lib/prisma'; +import { fail, redirect } from '@sveltejs/kit'; + +export async function load() { + const accounts = await prisma.account.findMany({ select: { id: true, name: true } }); + const users = await prisma.user.findMany({ select: { id: true, name: true } }); + return { accounts, users }; +} + +export const actions = { + create: async ({ request, locals }) => { + const form = await request.formData(); + const subject = form.get('title')?.toString().trim(); + const description = form.get('description')?.toString().trim(); + const accountId = form.get('accountId')?.toString(); + const dueDate = form.get('dueDate') ? new Date(form.get('dueDate')) : null; + const priority = form.get('priority')?.toString() || 'Medium'; + const ownerId = form.get('assignedId')?.toString(); + if (!subject || !accountId || !ownerId) { + return fail(400, { error: 'Missing required fields.' }); + } + const newCase = await prisma.case.create({ + data: { + subject, + description, + accountId, + dueDate, + priority, + ownerId, + organizationId: locals.org.id + } + }); + throw redirect(303, `/app/cases/${newCase.id}`); + } +}; diff --git a/src/routes/(app)/app/cases/new/+page.svelte b/src/routes/(app)/app/cases/new/+page.svelte new file mode 100644 index 0000000..e57a227 --- /dev/null +++ b/src/routes/(app)/app/cases/new/+page.svelte @@ -0,0 +1,64 @@ + + +
+

+ + Create New Case +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ {#if errorMsg} +
{errorMsg}
+ {/if} + +
+
diff --git a/src/routes/(app)/app/contacts/[contactId]/+page.server.js b/src/routes/(app)/app/contacts/[contactId]/+page.server.js new file mode 100644 index 0000000..5298b55 --- /dev/null +++ b/src/routes/(app)/app/contacts/[contactId]/+page.server.js @@ -0,0 +1,28 @@ +import prisma from '$lib/prisma'; + +export async function load({ params }) { + const contact = await prisma.contact.findUnique({ + where: { id: params.contactId } + }); + + if (!contact) { + return { + status: 404, + error: new Error('Contact not found') + }; + } + + // Get related accounts via AccountContactRelationship + const accountRels = await prisma.accountContactRelationship.findMany({ + where: { contactId: params.contactId }, + include: { account: true } + }); + const account = accountRels.length > 0 ? accountRels[0].account : null; + const isPrimary = accountRels.length > 0 ? accountRels[0].isPrimary : false; + const role = accountRels.length > 0 ? accountRels[0].role : null; + + // Optionally: fetch related tasks, events, etc. if you want to show them + // const tasks = await prisma.task.findMany({ where: { contactId: params.contactId } }); + + return { contact, account, isPrimary, role }; +} diff --git a/src/routes/(app)/app/contacts/[contactId]/+page.svelte b/src/routes/(app)/app/contacts/[contactId]/+page.svelte new file mode 100644 index 0000000..d9ede31 --- /dev/null +++ b/src/routes/(app)/app/contacts/[contactId]/+page.svelte @@ -0,0 +1,85 @@ + + +
+
+
+ + + + + Back to Account + +

{contact.firstName} {contact.lastName}

+ {#if contact.isPrimary} + Primary + {/if} +
+ Edit +
+ +
+
+

Title

+

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

+
+
+

Role

+

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

+
+
+

Email

+ {#if contact.email} + {contact.email} + {:else} + N/A + {/if} +
+
+

Phone

+ {#if contact.phone} + {contact.phone} + {:else} + N/A + {/if} +
+
+

Account

+ {contact.account?.name || 'N/A'} +
+
+

Created

+

{formatDate(contact.createdAt)}

+
+
+

Last Updated

+

{formatDate(contact.updatedAt)}

+
+
+ + {#if contact.activities && contact.activities.length} +
+

Recent Activities

+ +
+ {/if} +
diff --git a/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js b/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js new file mode 100644 index 0000000..3d27a66 --- /dev/null +++ b/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js @@ -0,0 +1,59 @@ +import prisma from '$lib/prisma'; +import { fail, redirect } from '@sveltejs/kit'; + +export async function load({ params }) { + const contact = await prisma.contact.findUnique({ + where: { id: params.contactId } + }); + if (!contact) { + return fail(404, { message: 'Contact not found' }); + } + // Get related account info + const accountRel = await prisma.accountContactRelationship.findFirst({ + where: { contactId: params.contactId }, + include: { account: true } + }); + return { + contact, + account: accountRel?.account || null, + isPrimary: accountRel?.isPrimary || false, + role: accountRel?.role || '' + }; +} + +export const actions = { + default: async ({ request, params }) => { + const formData = await request.formData(); + const firstName = formData.get('firstName')?.toString().trim(); + const lastName = formData.get('lastName')?.toString().trim(); + const email = formData.get('email')?.toString().trim() || null; + const phone = formData.get('phone')?.toString().trim() || null; + const title = formData.get('title')?.toString().trim() || null; + const description = formData.get('description')?.toString().trim() || null; + const isPrimary = formData.get('isPrimary') === 'true'; + const role = formData.get('role')?.toString().trim() || null; + + if (!firstName || !lastName) { + return fail(400, { message: 'First and last name are required.' }); + } + + // Update contact + await prisma.contact.update({ + where: { id: params.contactId }, + data: { firstName, lastName, email, phone, title, description } + }); + + // Update AccountContactRelationship if exists + const accountRel = await prisma.accountContactRelationship.findFirst({ + where: { contactId: params.contactId } + }); + if (accountRel) { + await prisma.accountContactRelationship.update({ + where: { id: accountRel.id }, + data: { isPrimary, role } + }); + } + + return { success: true }; + } +}; diff --git a/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte b/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte new file mode 100644 index 0000000..ed8c3ce --- /dev/null +++ b/src/routes/(app)/app/contacts/[contactId]/edit/+page.svelte @@ -0,0 +1,111 @@ + + +
+

Edit Contact

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ {#if errorMsg} +
{errorMsg}
+ {/if} +
+ + +
+
+
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 d6a674a..4eeca70 100644 --- a/src/routes/(app)/app/leads/[lead_id]/+page.server.js +++ b/src/routes/(app)/app/leads/[lead_id]/+page.server.js @@ -220,54 +220,56 @@ export const actions = { }, // Action to add a comment to the lead - addComment: async ({ params, request }) => { + addComment: async ({ params, request, locals }) => { const lead_id = params.lead_id; const data = await request.formData(); const comment = data.get('comment'); - - if (!comment || comment.trim() === '') { + const commentValue = typeof comment === 'string' ? comment : String(comment); + if (!commentValue.trim()) { return fail(400, { status: 'error', message: 'Comment cannot be empty' }); } - try { - // For now, we'll use a fixed user ID. In a real app, you would use the logged-in user's ID - const CURRENT_USER_ID = 'user_01'; // Replace with actual user auth - + // Use the logged-in user from locals + const user = locals.user; + if (!user) { + return fail(401, { + status: 'error', + message: 'You must be logged in to comment.' + }); + } // Get the lead to obtain its organization ID const lead = await prisma.lead.findUnique({ where: { id: lead_id }, select: { organizationId: true } }); - if (!lead) { return fail(404, { status: 'error', message: 'Lead not found' }); } - - const newComment = await prisma.comment.create({ + await prisma.comment.create({ data: { - body: comment, - lead: { - connect: { id: lead_id } - }, - author: { - connect: { id: CURRENT_USER_ID } - }, - organization: { - connect: { id: lead.organizationId } - } + body: commentValue, + lead: { connect: { id: lead_id } }, + author: { connect: { id: user.id } }, + organization: { connect: { id: lead.organizationId } } + } + }); + // Refetch comments for immediate update + const updatedLead = await prisma.lead.findUnique({ + where: { id: lead_id }, + include: { + comments: { include: { author: true }, orderBy: { createdAt: 'desc' } } } }); - return { status: 'success', message: 'Comment added successfully', - comment: newComment + comments: updatedLead?.comments || [] }; } catch (err) { console.error('Error adding comment:', err); diff --git a/src/routes/(app)/app/leads/[lead_id]/+page.svelte b/src/routes/(app)/app/leads/[lead_id]/+page.svelte index db22ae5..6740673 100644 --- a/src/routes/(app)/app/leads/[lead_id]/+page.svelte +++ b/src/routes/(app)/app/leads/[lead_id]/+page.svelte @@ -344,9 +344,14 @@
-
{ + { isSubmittingComment = true; - return async ({ update }) => { + return async ({ result }) => { + isSubmittingComment = false; + if (result?.type === 'success' && result?.data?.comments) { + lead.comments = result.data.comments; + newComment = ''; + } await update({ reset: false }); }; }}> @@ -402,137 +407,6 @@
- - -
-
-

Tasks

- -
- - {#if lead.tasks && lead.tasks.length > 0} -
- {#each lead.tasks as task, i} -
-
-
-
- - {#if task.completed} -
- - - -
- {/if} -
-
{task.subject}
-
- {task.priority} -
- {#if task.description} -

{task.description}

- {/if} -
-
- - - - Due: {formatDate(task.dueDate)} -
- -
-
- {/each} -
- {:else} -
-
- - - -

No tasks found

-

Create a new task for this lead

-
-
- {/if} -
-
- - -
-
-

Events

- -
- - {#if lead.events && lead.events.length > 0} -
- {#each lead.events as event, i} -
-
-
-
{new Date(event.startDate).toLocaleString('default', { month: 'short' })}
-
{new Date(event.startDate).getDate()}
-
-
-
{event.subject}
-
- - - - {formatDate(event.startDate)} - {formatDate(event.endDate)} -
- {#if event.location} -
- - - - - {event.location} -
- {/if} - {#if event.description} -

{event.description}

- {/if} -
- -
-
- {/each} -
- {:else} -
-
- - - -

No events found

-

Schedule a new event for this lead

-
-
- {/if} -
-
diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/+page.server.js b/src/routes/(app)/app/opportunities/[opportunityId]/+page.server.js new file mode 100644 index 0000000..81aa64c --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/+page.server.js @@ -0,0 +1,20 @@ +import { error } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; + +export async function load({ params }) { + const opportunity = await prisma.opportunity.findUnique({ + where: { id: params.opportunityId }, + include: { + account: true, + owner: true + } + }); + if (!opportunity) { + throw error(404, 'Opportunity not found'); + } + return { + opportunity, + account: opportunity.account, + owner: opportunity.owner + }; +} diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/+page.svelte b/src/routes/(app)/app/opportunities/[opportunityId]/+page.svelte new file mode 100644 index 0000000..9cfe1a8 --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/+page.svelte @@ -0,0 +1,68 @@ + + +
+
+
+ Back to Account + / +

{opportunity.name}

+
+
+ Edit + Delete +
+
+
+
+
+
Stage
+
{opportunity.stage}
+
+
+
Amount
+
${opportunity.amount?.toLocaleString() ?? 'N/A'}
+
+
+
Probability
+
{opportunity.probability ? `${opportunity.probability}%` : 'N/A'}
+
+
+
Close Date
+
{opportunity.closeDate ? new Date(opportunity.closeDate).toLocaleDateString() : 'N/A'}
+
+
+
Account
+
{account?.name ?? 'N/A'}
+
+
+
Owner
+
{owner?.name ?? 'N/A'}
+
+
+
+
Description
+
{opportunity.description || 'No description'}
+
+
+
+
Created
+
{opportunity.createdAt ? new Date(opportunity.createdAt).toLocaleString() : 'N/A'}
+
+
+
Last Updated
+
{opportunity.updatedAt ? new Date(opportunity.updatedAt).toLocaleString() : 'N/A'}
+
+
+
+
+ diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.server.js b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.server.js new file mode 100644 index 0000000..3fedab4 --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.server.js @@ -0,0 +1,21 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; + +export async function load({ params }) { + const opportunity = await prisma.opportunity.findUnique({ + where: { id: params.opportunityId } + }); + if (!opportunity) throw error(404, 'Opportunity not found'); + return { opportunity }; +} + +export const actions = { + default: async ({ params }) => { + try { + await prisma.opportunity.delete({ where: { id: params.opportunityId } }); + throw redirect(303, `/app/accounts/${params.accountId}`); + } catch (err) { + return fail(500, { message: 'Failed to delete opportunity.' }); + } + } +}; diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.svelte b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.svelte new file mode 100644 index 0000000..21a7545 --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/delete/+page.svelte @@ -0,0 +1,37 @@ + +
+

Delete Opportunity

+

Are you sure you want to delete the opportunity {opportunity.name}? This action cannot be undone.

+ {#if error} +
{error}
+ {/if} + + + Cancel + +
+ diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.server.js b/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.server.js new file mode 100644 index 0000000..46ddb32 --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.server.js @@ -0,0 +1,32 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import prisma from '$lib/prisma'; + +export async function load({ params }) { + const opportunity = await prisma.opportunity.findUnique({ + where: { id: params.opportunityId } + }); + if (!opportunity) throw error(404, 'Opportunity not found'); + return { opportunity }; +} + +export const actions = { + default: async ({ request, params }) => { + const form = await request.formData(); + const name = form.get('name')?.toString().trim(); + const amount = form.get('amount') ? parseFloat(form.get('amount')) : null; + const stage = form.get('stage')?.toString(); + const probability = form.get('probability') ? parseFloat(form.get('probability')) : null; + const closeDate = form.get('closeDate') ? new Date(form.get('closeDate')) : null; + const description = form.get('description')?.toString(); + if (!name) return fail(400, { message: 'Name is required.' }); + try { + await prisma.opportunity.update({ + where: { id: params.opportunityId }, + data: { name, amount, stage, probability, closeDate, description } + }); + throw redirect(303, `/app/opportunities/${params.opportunityId}`); + } catch (err) { + return fail(500, { message: 'Failed to update opportunity.' }); + } + } +}; diff --git a/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte b/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte new file mode 100644 index 0000000..9f32e1c --- /dev/null +++ b/src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte @@ -0,0 +1,75 @@ + + +
+

Edit Opportunity

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {#if error} +
{error}
+ {/if} +
+ + Cancel +
+
+
+ diff --git a/src/routes/(app)/cases/+page.svelte b/src/routes/(app)/cases/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/(app)/cases/[caseId]/+page.svelte b/src/routes/(app)/cases/[caseId]/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/(app)/cases/[caseId]/edit/+page.svelte b/src/routes/(app)/cases/[caseId]/edit/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/(app)/cases/new/+page.svelte b/src/routes/(app)/cases/new/+page.svelte new file mode 100644 index 0000000..e69de29