diff --git a/package.json b/package.json index f594e66..789629e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "dependencies": { "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@lucide/svelte": "^0.511.0", "@prisma/client": "6.5.0", "axios": "^1.8.4", "date-fns": "^4.1.0", @@ -53,6 +54,7 @@ "flowbite-svelte-icons": "^2.1.1", "marked": "^15.0.8", "svelte-fa": "^4.0.3", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "zod": "^3.25.28" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 140ae3a..31534cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fortawesome/free-solid-svg-icons': specifier: ^6.7.2 version: 6.7.2 + '@lucide/svelte': + specifier: ^0.511.0 + version: 0.511.0(svelte@5.27.0) '@prisma/client': specifier: 6.5.0 version: 6.5.0(prisma@6.5.0(typescript@5.8.3))(typescript@5.8.3) @@ -38,6 +41,9 @@ importers: uuid: specifier: ^11.1.0 version: 11.1.0 + zod: + specifier: ^3.25.28 + version: 3.25.28 devDependencies: '@eslint/compat': specifier: ^1.2.5 @@ -375,6 +381,11 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lucide/svelte@0.511.0': + resolution: {integrity: sha512-aLCSPMUJmHlCuLXzXENXa4Z1NV2mN1iAZAFKk4bEbey+/MdsNlu+/DqwVkgW3Yvj6p8y8Vn5xZ2v9CLmPlA6Vw==} + peerDependencies: + svelte: ^5 + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1635,6 +1646,9 @@ packages: zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod@3.25.28: + resolution: {integrity: sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==} + snapshots: '@ampproject/remapping@2.3.0': @@ -1820,6 +1834,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@lucide/svelte@0.511.0(svelte@5.27.0)': + dependencies: + svelte: 5.27.0 + '@polka/url@1.0.0-next.29': {} '@popperjs/core@2.11.8': {} @@ -2935,3 +2953,5 @@ snapshots: yocto-queue@0.1.0: {} zimmerframe@1.1.2: {} + + zod@3.25.28: {} diff --git a/prisma/migrations/20250524154126_/migration.sql b/prisma/migrations/20250524154126_/migration.sql new file mode 100644 index 0000000..bf5a78a --- /dev/null +++ b/prisma/migrations/20250524154126_/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Comment" ADD COLUMN "taskId" TEXT; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9c6abf6..e317499 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,8 +30,8 @@ model User { contacts Contact[] leads Lead[] opportunities Opportunity[] - tasks Task[] - events Event[] + tasks Task[] // Tasks created by user + events Event[] // Events created by user ownedTasks Task[] @relation("TaskOwner") ownedEvents Event[] @relation("EventOwner") cases Case[] @@ -281,6 +281,7 @@ model Task { caseId String? organization Organization @relation(fields: [organizationId], references: [id]) organizationId String + comments Comment[] @relation("TaskComments") } model Event { @@ -404,6 +405,8 @@ model Comment { accountId String? contact Contact? @relation(fields: [contactId], references: [id]) contactId String? + task Task? @relation("TaskComments", fields: [taskId], references: [id]) + taskId String? } model Quote { diff --git a/src/routes/(app)/Sidebar.svelte b/src/routes/(app)/Sidebar.svelte index 82a1e34..fea9480 100644 --- a/src/routes/(app)/Sidebar.svelte +++ b/src/routes/(app)/Sidebar.svelte @@ -2,7 +2,7 @@ import { afterNavigate } from '$app/navigation'; import { page } from '$app/stores'; import Fa from 'svelte-fa'; - import { faPieChart } from '@fortawesome/free-solid-svg-icons'; + import { faPieChart, faQuestion } from '@fortawesome/free-solid-svg-icons'; import { Sidebar, SidebarDropdownItem, @@ -134,6 +134,20 @@ class={`${mainSidebarUrl === '/app/invoices/new' ? 'bg-gray-100 font-semibold dark:bg-gray-700' : ''}`} /> --> + + + + + + diff --git a/src/routes/(app)/app/accounts/+page.server.js b/src/routes/(app)/app/accounts/+page.server.js index 43a8d63..aed5cfb 100644 --- a/src/routes/(app)/app/accounts/+page.server.js +++ b/src/routes/(app)/app/accounts/+page.server.js @@ -1,8 +1,9 @@ import { error } from '@sveltejs/kit'; import prisma from '$lib/prisma'; -export async function load({ locals, url }) { - +export async function load({ locals, url, params }) { + const org = locals.org; + const page = parseInt(url.searchParams.get('page') || '1'); const limit = parseInt(url.searchParams.get('limit') || '10'); const sort = url.searchParams.get('sort') || 'name'; @@ -12,7 +13,7 @@ export async function load({ locals, url }) { try { // Build the where clause for filtering - const where = {}; + const where = {organizationId: org.id}; // Add status filter const status = url.searchParams.get('status'); diff --git a/src/routes/(app)/app/accounts/[accountId]/+page.server.js b/src/routes/(app)/app/accounts/[accountId]/+page.server.js index 5e0a31d..da3cdd8 100644 --- a/src/routes/(app)/app/accounts/[accountId]/+page.server.js +++ b/src/routes/(app)/app/accounts/[accountId]/+page.server.js @@ -2,14 +2,17 @@ import { error, fail } from '@sveltejs/kit'; import prisma from '$lib/prisma'; /** @type {import('./$types').PageServerLoad} */ -export async function load({ params, url }) { +export async function load({ params, url, locals }) { + const user = locals.user; + const org = locals.org; try { const accountId = params.accountId; // Fetch account details const account = await prisma.account.findUnique({ where: { - id: accountId + id: accountId, + organizationId: org.id } }); @@ -127,10 +130,7 @@ export const actions = { closeAccount: async ({ params, request, locals }) => { try { const user = locals.user; - - if (!user) { - return fail(401, { success: false, message: 'Unauthorized' }); - } + const org = locals.org; const { accountId } = params; const formData = await request.formData(); @@ -142,7 +142,7 @@ export const actions = { // Fetch the account to verify it exists const account = await prisma.account.findUnique({ - where: { id: accountId }, + where: { id: accountId, organizationId: org.id }, select: { id: true, closedAt: true, @@ -169,8 +169,7 @@ export const actions = { const hasPermission = user.id === account.ownerId || - userOrg?.role === 'ADMIN' || - userOrg?.role === 'SALES_MANAGER'; + userOrg?.role === 'ADMIN'; if (!hasPermission) { return fail(403, { success: false, message: 'Permission denied. Only account owners, sales managers, or admins can close accounts.' }); @@ -210,16 +209,13 @@ export const actions = { reopenAccount: async ({ params, request, locals }) => { try { const user = locals.user; - - if (!user) { - return fail(401, { success: false, message: 'Unauthorized' }); - } + const org = locals.org; const { accountId } = params; // Fetch the account to verify it exists const account = await prisma.account.findUnique({ - where: { id: accountId }, + where: { id: accountId, organizationId: org.id }, select: { id: true, closedAt: true, @@ -247,8 +243,7 @@ export const actions = { const hasPermission = user.id === account.ownerId || - userOrg?.role === 'ADMIN' || - userOrg?.role === 'SALES_MANAGER'; + userOrg?.role === 'ADMIN'; if (!hasPermission) { return fail(403, { success: false, message: 'Permission denied. Only account owners, sales managers, or admins can reopen accounts.' }); @@ -294,9 +289,8 @@ export const actions = { addContact: async ({ params, request, locals }) => { try { const user = locals.user; - if (!user) { - return fail(401, { success: false, message: 'Unauthorized' }); - } + const org = locals.org; + const { accountId } = params; let data; // Support both JSON and form submissions @@ -312,6 +306,14 @@ export const actions = { if (!firstName || !lastName) { return fail(400, { success: false, message: 'First and last name are required.' }); } + + // check if the account exists and belongs to the organization + const account = await prisma.account.findUnique({ + where: { id: accountId, organizationId: org.id } + }); + if (!account) { + return fail(404, { success: false, message: 'Account not found or does not belong to this organization.' }); + } // Create the contact const contact = await prisma.contact.create({ data: { @@ -321,7 +323,7 @@ export const actions = { phone: data.phone?.toString() || null, title: data.title?.toString() || null, ownerId: user.id, - organizationId: (await prisma.account.findUnique({ where: { id: accountId }, select: { organizationId: true } })).organizationId + organizationId: org.id, } }); // Link contact to account @@ -342,13 +344,9 @@ export const actions = { addOpportunity: async ({ params, request, locals }) => { try { - // @ts-ignore const user = locals.user; - // @ts-ignore const org = locals.org; - if (!user || !org) { - return fail(401, { success: false, message: 'Unauthorized' }); - } + const { accountId } = params; const formData = await request.formData(); const name = formData.get('name')?.toString().trim(); @@ -363,6 +361,15 @@ export const actions = { if (!name) { return fail(400, { success: false, message: 'Opportunity name is required.' }); } + + // chek if the account exists and belongs to the organization + const account = await prisma.account.findUnique({ + where: { id: accountId, organizationId: org.id } + }); + if (!account) { + return fail(404, { success: false, message: 'Account not found or does not belong to this organization.' }); + } + // Create the opportunity await prisma.opportunity.create({ data: { @@ -385,9 +392,10 @@ export const actions = { comment: async ({ request, params, locals }) => { const user = locals.user; + const org = locals.org; // Fallback: fetch account to get organizationId const account = await prisma.account.findUnique({ - where: { id: params.accountId }, + where: { id: params.accountId, organizationId: org.id }, select: { organizationId: true, ownerId: true } }); if (!account) { @@ -414,9 +422,7 @@ export const actions = { try { const user = locals.user; const org = locals.org; - if (!user || !org) { - return fail(401, { success: false, message: 'Unauthorized' }); - } + const { accountId } = params; const formData = await request.formData(); const subject = formData.get('subject')?.toString().trim(); @@ -427,9 +433,17 @@ export const actions = { if (!subject) { return fail(400, { success: false, message: 'Subject is required.' }); } + + // Check if the account exists and belongs to the organization + const account = await prisma.account.findUnique({ + where: { id: accountId, organizationId: org.id } + }); + if (!account) { + return fail(404, { success: false, message: 'Account not found or does not belong to this organization.' }); + } // If no ownerId is provided, default to current user // if (!ownerId) ownerId = user.id; - console.log(user.id, org.id); + // console.log(user.id, org.id); const task = await prisma.task.create({ data: { subject, diff --git a/src/routes/(app)/app/accounts/[accountId]/delete/+page.server.js b/src/routes/(app)/app/accounts/[accountId]/delete/+page.server.js index 7fd08e6..68430f9 100644 --- a/src/routes/(app)/app/accounts/[accountId]/delete/+page.server.js +++ b/src/routes/(app)/app/accounts/[accountId]/delete/+page.server.js @@ -1,12 +1,13 @@ import prisma from '$lib/prisma'; import { error, fail, redirect } from '@sveltejs/kit'; -export async function load({ params }) { +export async function load({ params, locals }) { + const org = locals.org; try { const accountId = params.accountId; const account = await prisma.account.findUnique({ - where: { id: accountId }, + where: { id: accountId, organizationId: org.id }, select: { id: true, name: true, @@ -72,13 +73,14 @@ export async function load({ params }) { } export const actions = { - default: async ({ params }) => { + default: async ({ params, locals }) => { try { const accountId = params.accountId; + const org = locals.org; // Check if account exists first const account = await prisma.account.findUnique({ - where: { id: accountId }, + where: { id: accountId, organizationId: org.id }, include: { _count: { select: { diff --git a/src/routes/(app)/app/accounts/[accountId]/edit/+page.server.js b/src/routes/(app)/app/accounts/[accountId]/edit/+page.server.js index 2ab8502..3e40d0c 100644 --- a/src/routes/(app)/app/accounts/[accountId]/edit/+page.server.js +++ b/src/routes/(app)/app/accounts/[accountId]/edit/+page.server.js @@ -2,9 +2,9 @@ import { fail, redirect, error } from '@sveltejs/kit'; import prisma from '$lib/prisma'; /** @type {import('./$types').PageServerLoad} */ -export async function load({ params }) { +export async function load({ params, locals }) { const account = await prisma.account.findUnique({ - where: { id: params.accountId } + where: { id: params.accountId, organizationId: locals.org.id } }); if (!account) throw error(404, 'Account not found'); return { account }; @@ -12,7 +12,8 @@ export async function load({ params }) { /** @type {import('./$types').Actions} */ export const actions = { - default: async ({ request, params }) => { + default: async ({ request, params, locals }) => { + const org = locals.org; const form = await request.formData(); const name = form.get('name'); const industry = form.get('industry'); @@ -25,7 +26,7 @@ export const actions = { } await prisma.account.update({ - where: { id: params.accountId }, + where: { id: params.accountId, organizationId: org.id }, data: { name, industry, type, website, phone } }); throw redirect(303, `/app/accounts/${params.accountId}`); diff --git a/src/routes/(app)/app/cases/+page.server.js b/src/routes/(app)/app/cases/+page.server.js index 876d26c..956ec3a 100644 --- a/src/routes/(app)/app/cases/+page.server.js +++ b/src/routes/(app)/app/cases/+page.server.js @@ -2,6 +2,8 @@ import { fail, redirect, error } from '@sveltejs/kit'; import prisma from '$lib/prisma'; export async function load({ url, locals }) { + const org = locals.org; + const user = locals.user; // Filters from query params const status = url.searchParams.get('status') || undefined; const assigned = url.searchParams.get('assigned') || undefined; @@ -23,7 +25,7 @@ export async function load({ url, locals }) { const statusOptions = ['OPEN', 'IN_PROGRESS', 'CLOSED']; const cases = await prisma.case.findMany({ - where, + where: { organizationId: org.id }, include: { owner: { select: { id: true, name: true } }, account: { select: { id: true, name: true } }, diff --git a/src/routes/(app)/app/cases/[caseId]/+page.server.js b/src/routes/(app)/app/cases/[caseId]/+page.server.js index b1ca863..fff8c52 100644 --- a/src/routes/(app)/app/cases/[caseId]/+page.server.js +++ b/src/routes/(app)/app/cases/[caseId]/+page.server.js @@ -1,10 +1,11 @@ import prisma from '$lib/prisma'; import { error, fail, redirect } from '@sveltejs/kit'; -export async function load({ params }) { +export async function load({ params, locals }) { + const org = locals.org; const caseId = params.caseId; const caseItem = await prisma.case.findUnique({ - where: { id: caseId }, + where: { id: caseId, organizationId: org.id }, include: { owner: { select: { id: true, name: true } }, account: { select: { id: true, name: true } }, @@ -20,6 +21,15 @@ export async function load({ params }) { export const actions = { comment: async ({ request, params, locals }) => { + const org = locals.org; + + // check if the case is related to the organization + const caseExists = await prisma.case.findFirst({ + where: { id: params.caseId, organizationId: org.id } + }); + if (!caseExists) { + return fail(404, { error: 'Case not found or does not belong to this organization.' }); + } const form = await request.formData(); const body = form.get('body')?.toString().trim(); if (!body) return fail(400, { error: 'Comment cannot be empty.' }); diff --git a/src/routes/(app)/app/cases/[caseId]/edit/+page.server.js b/src/routes/(app)/app/cases/[caseId]/edit/+page.server.js index b47c8fc..b6664c5 100644 --- a/src/routes/(app)/app/cases/[caseId]/edit/+page.server.js +++ b/src/routes/(app)/app/cases/[caseId]/edit/+page.server.js @@ -1,10 +1,11 @@ import prisma from '$lib/prisma'; import { fail, redirect, error } from '@sveltejs/kit'; -export async function load({ params }) { +export async function load({ params, locals }) { + const org = locals.org; const caseId = params.caseId; const caseItem = await prisma.case.findUnique({ - where: { id: caseId }, + where: { id: caseId, organizationId: org.id }, include: { owner: { select: { id: true, name: true } }, account: { select: { id: true, name: true } } @@ -20,7 +21,8 @@ export async function load({ params }) { } export const actions = { - update: async ({ request, params }) => { + update: async ({ request, params, locals }) => { + const org = locals.org; const form = await request.formData(); const subject = form.get('title')?.toString().trim(); const description = form.get('description')?.toString().trim(); @@ -32,6 +34,13 @@ export const actions = { if (!subject || !accountId || !ownerId) { return fail(400, { error: 'Missing required fields.' }); } + // Validate case is part of the organization + const caseExists = await prisma.case.findFirst({ + where: { id: params.caseId, organizationId: org.id } + }); + if (!caseExists) { + return fail(404, { error: 'Case not found or does not belong to this organization.' }); + } await prisma.case.update({ where: { id: params.caseId }, data: { subject, description, accountId, dueDate, priority, ownerId } diff --git a/src/routes/(app)/app/cases/new/+page.server.js b/src/routes/(app)/app/cases/new/+page.server.js index aa1ff2d..a7435bc 100644 --- a/src/routes/(app)/app/cases/new/+page.server.js +++ b/src/routes/(app)/app/cases/new/+page.server.js @@ -1,14 +1,31 @@ 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 } }); +export async function load({ locals }) { + const org = locals.org; + const accounts = await prisma.account.findMany( + { + where: { organizationId: org.id }, + select: { id: true, name: true } + } + ); + const users = await prisma.userOrganization.findMany({ + where: { organizationId: org.id }, + select: { + user: { + select: { + id: true, + name: true + } + } + } + }); return { accounts, users }; } export const actions = { create: async ({ request, locals }) => { + const org = locals.org; const form = await request.formData(); const subject = form.get('title')?.toString().trim(); const description = form.get('description')?.toString().trim(); @@ -19,6 +36,18 @@ export const actions = { if (!subject || !accountId || !ownerId) { return fail(400, { error: 'Missing required fields.' }); } + + // check if the ownerId is valid and related to the organization + const isValidOwner = await prisma.userOrganization.findFirst({ + where: { + userId: ownerId, + organizationId: org.id + } + }); + if (!isValidOwner) { + return fail(400, { error: 'Invalid owner ID.' }); + } + const newCase = await prisma.case.create({ data: { subject, diff --git a/src/routes/(app)/app/contacts/[contactId]/+page.server.js b/src/routes/(app)/app/contacts/[contactId]/+page.server.js index 5298b55..9abaf6f 100644 --- a/src/routes/(app)/app/contacts/[contactId]/+page.server.js +++ b/src/routes/(app)/app/contacts/[contactId]/+page.server.js @@ -1,8 +1,9 @@ import prisma from '$lib/prisma'; -export async function load({ params }) { +export async function load({ params, locals }) { + const org = locals.org; const contact = await prisma.contact.findUnique({ - where: { id: params.contactId } + where: { id: params.contactId, organizationId: org.id }, }); if (!contact) { diff --git a/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js b/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js index 3d27a66..ed3c884 100644 --- a/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js +++ b/src/routes/(app)/app/contacts/[contactId]/edit/+page.server.js @@ -1,9 +1,12 @@ import prisma from '$lib/prisma'; import { fail, redirect } from '@sveltejs/kit'; -export async function load({ params }) { +export async function load({ params, locals }) { + const org = locals.org; + const user = locals.user; + const contact = await prisma.contact.findUnique({ - where: { id: params.contactId } + where: { id: params.contactId, organizationId: org.id } }); if (!contact) { return fail(404, { message: 'Contact not found' }); @@ -22,7 +25,10 @@ export async function load({ params }) { } export const actions = { - default: async ({ request, params }) => { + default: async ({ request, params, locals }) => { + const org = locals.org; + const user = locals.user; + const formData = await request.formData(); const firstName = formData.get('firstName')?.toString().trim(); const lastName = formData.get('lastName')?.toString().trim(); @@ -37,6 +43,14 @@ export const actions = { return fail(400, { message: 'First and last name are required.' }); } + + const contact = await prisma.contact.findUnique({ + where: { id: params.contactId, organizationId: org.id } + }); + if (!contact) { + return fail(404, { message: 'Contact not found' }); + } + // Update contact await prisma.contact.update({ where: { id: params.contactId }, 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 4eeca70..c7b01ed 100644 --- a/src/routes/(app)/app/leads/[lead_id]/+page.server.js +++ b/src/routes/(app)/app/leads/[lead_id]/+page.server.js @@ -1,77 +1,71 @@ import { error, fail } from '@sveltejs/kit'; import { PrismaClient } from '@prisma/client'; +import { z } from 'zod'; // For input validation const prisma = new PrismaClient(); -export async function load({ params }) { +// Input validation schemas +const commentSchema = z.object({ + comment: z.string().min(1, 'Comment cannot be empty').max(1000, 'Comment too long').trim() +}); + +export async function load({ params, locals }) { const lead_id = params.lead_id; - - try { - // Fetch lead with owner information - const lead = await prisma.lead.findUnique({ - where: { id: lead_id }, - include: { - owner: true, - tasks: { - orderBy: { createdAt: 'desc' } - }, - events: { - orderBy: { startDate: 'asc' } - }, - comments: { - include: { - author: true - }, - orderBy: { createdAt: 'desc' } + const org = locals.org; + + const lead = await prisma.lead.findUnique({ + where: { id: lead_id, organizationId: org.id }, + include: { + owner: true, + tasks: { + orderBy: { createdAt: 'desc' } + }, + events: { + orderBy: { startDate: 'asc' } + }, + comments: { + include: { + author: true }, - contact: true - } - }); - console.log('Lead:', lead); - if (!lead) { - throw error(404, 'Lead not found'); + orderBy: { createdAt: 'desc' } + }, + contact: true } - - return { - lead - }; - } catch (err) { - console.error('Error fetching lead:', err); - throw error(500, 'Failed to load lead details'); + }); + + if (!lead) { + throw error(404, 'Lead not found'); } + + console.log('Loaded lead:', lead); + return { + lead + }; } -// Add form actions export const actions = { - // Action to convert a lead to contact/account/opportunity - convert: async ({ params }) => { + convert: async ({ params, locals }) => { const lead_id = params.lead_id; - + const user = locals.user; + const org = locals.org; + try { - // Get the lead to convert with organization data const lead = await prisma.lead.findUnique({ - where: { id: lead_id }, + where: { id: lead_id, organizationId: org.id }, include: { organization: true, owner: true } }); - + if (!lead) { - return fail(404, { - status: 'error', - message: 'Lead not found' - }); + return fail(404, { status: 'error', message: 'Lead not found' }); } - + if (lead.status === 'CONVERTED') { - return { - status: 'success', - message: 'Lead already converted' - }; + return { status: 'success', message: 'Lead already converted' }; } - - // Create a new contact from the lead + const contact = await prisma.contact.create({ data: { firstName: lead.firstName, @@ -80,17 +74,11 @@ export const actions = { phone: lead.phone, title: lead.title, description: lead.description, - // Connect to the required relationships - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } } }); - - // Create a new account if company info exists + let accountId = null; let account = null; if (lead.company) { @@ -98,96 +86,60 @@ export const actions = { data: { name: lead.company, industry: lead.industry, - // Connect to the required relationships - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } } }); accountId = account.id; - - // Create the relationship between account and contact using AccountContactRelationship model + await prisma.accountContactRelationship.create({ data: { - account: { - connect: { id: account.id } - }, - contact: { - connect: { id: contact.id } - }, + account: { connect: { id: account.id } }, + contact: { connect: { id: contact.id } }, isPrimary: true, role: 'Primary Contact' } }); } - - // Create an opportunity - // First build the common data for the opportunity + const opportunityData = { name: `${lead.company || lead.firstName + ' ' + lead.lastName} Opportunity`, stage: 'PROSPECTING', amount: 0, - closeDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now - - // Connect to the required relationships - contacts: { - connect: { id: contact.id } - }, - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + closeDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + contacts: { connect: { id: contact.id } }, + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } }; - - // According to the schema, an opportunity must be connected to an account - // If no account was created, we'll need to create a placeholder account + if (!accountId) { const placeholderAccount = await prisma.account.create({ data: { name: `${lead.firstName} ${lead.lastName} Account`, - // Connect to the required relationships - owner: { - connect: { id: lead.ownerId } - }, - organization: { - connect: { id: lead.organizationId } - } + owner: { connect: { id: lead.ownerId } }, + organization: { connect: { id: lead.organizationId } } } }); - + accountId = placeholderAccount.id; account = placeholderAccount; - - // Create relationship between contact and placeholder account + await prisma.accountContactRelationship.create({ data: { - account: { - connect: { id: placeholderAccount.id } - }, - contact: { - connect: { id: contact.id } - }, + account: { connect: { id: placeholderAccount.id } }, + contact: { connect: { id: contact.id } }, isPrimary: true, role: 'Primary Contact' } }); } - - // Now add the account to the opportunity data - opportunityData.account = { - connect: { id: accountId } - }; - + + opportunityData.account = { connect: { id: accountId } }; + const opportunity = await prisma.opportunity.create({ data: opportunityData }); - - // Update the lead as converted + await prisma.lead.update({ where: { id: lead_id }, data: { @@ -197,12 +149,10 @@ export const actions = { convertedContactId: contact.id, convertedAccountId: accountId, convertedOpportunityId: opportunity.id, - contact: { - connect: { id: contact.id } - } + contact: { connect: { id: contact.id } } } }); - + return { status: 'success', message: 'Lead successfully converted', @@ -211,72 +161,60 @@ export const actions = { opportunity }; } catch (err) { - console.error('Error converting lead:', err); - return fail(500, { - status: 'error', - message: 'Failed to convert lead: ' + (err.message || 'Unknown error') - }); + console.error('Error converting lead:', err.message); + return fail(500, { status: 'error', message: 'Failed to convert lead' }); } }, - - // Action to add a comment to the lead + addComment: async ({ params, request, locals }) => { const lead_id = params.lead_id; + const user = locals.user; + const org = locals.org; + + + // Validate form data const data = await request.formData(); const comment = data.get('comment'); - const commentValue = typeof comment === 'string' ? comment : String(comment); - if (!commentValue.trim()) { - return fail(400, { - status: 'error', - message: 'Comment cannot be empty' - }); - } + try { - // 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 validatedComment = commentSchema.parse({ comment }); + const lead = await prisma.lead.findUnique({ - where: { id: lead_id }, + where: { id: lead_id, organizationId: org.id }, select: { organizationId: true } }); + if (!lead) { - return fail(404, { - status: 'error', - message: 'Lead not found' - }); + return fail(404, { status: 'error', message: 'Lead not found' }); } + await prisma.comment.create({ data: { - body: commentValue, + body: validatedComment.comment, 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', comments: updatedLead?.comments || [] }; } catch (err) { - console.error('Error adding comment:', err); - return fail(500, { - status: 'error', - message: 'Failed to add comment' - }); + console.error('Error adding comment:', err.message); + if (err instanceof z.ZodError) { + return fail(400, { status: 'error', message: err.errors[0].message }); + } + return fail(500, { status: 'error', message: 'Failed to add comment' }); } } -}; +}; \ No newline at end of file diff --git a/src/routes/(app)/app/leads/[lead_id]/+page.svelte b/src/routes/(app)/app/leads/[lead_id]/+page.svelte index 6740673..d124e8a 100644 --- a/src/routes/(app)/app/leads/[lead_id]/+page.svelte +++ b/src/routes/(app)/app/leads/[lead_id]/+page.svelte @@ -1,9 +1,9 @@ - {#if showToast} - + {toastMessage} {/if} -
- -
-
-
- - - - - -

{getFullName(lead)}

- {lead.status} -
-
- {#if lead.status !== 'CONVERTED'} - -
{ - isConverting = true; // Set loading state on submit - return async ({ update }) => { - // This runs after the action completes - await update({ reset: false }); // Update form prop without resetting the page - // isConverting will be reset by the reactive statement above - }; - }}> - -
- {/if} - -
-
-
+
+
+
+ +
+ + -
- -
- -
- - -
-
- - - -
-
-

{getFullName(lead)}

-
- {lead.status} -
- {#if lead.title} -
{lead.title}
- {/if} - {#if lead.company} -
{lead.company}
- {/if} + +
+
+ +
+
+

{getFullName(lead)}

+
+

Lead

+ {lead.status}
- - -
-

Contact Information

- {#if lead.email} -
-
- - - -
- {lead.email} -
- {/if} - {#if lead.phone} -
-
- - - -
- {lead.phone} -
+
+ {#if lead.status !== 'CONVERTED'} +
+ +
{/if} +
+
- -
-

Lead Details

-
- {#if lead.leadSource} -
- Source - {lead.leadSource.replace('_', ' ').toLowerCase()} -
- {/if} - {#if lead.industry} -
- Industry - {lead.industry} -
- {/if} - {#if lead.rating} -
- Rating -
- {#each Array(parseInt(lead.rating) || 0) as _, i} - - - + +
+ +
+
+

Lead Information

+
+ {#each [ + { label: 'Full Name', value: getFullName(lead) }, + { label: 'Company', value: lead.company }, + { label: 'Email', value: lead.email, href: `mailto:${lead.email}` }, + { label: 'Phone', value: lead.phone, href: `tel:${lead.phone}` }, + { label: 'Lead Source', value: lead.leadSource?.replace('_', ' ')?.toLowerCase(), capitalize: true }, + { label: 'Industry', value: lead.industry } + ] as item} + {#if item.value} +
+

{item.label}

+ {#if item.href} +

{item.value}

+ {:else} +

{item.value}

+ {/if} +
+ {/if} {/each}
-
- {/if} -
-
+ - -
-

Timeline

-
-
-
-
- Created - {formatDate(lead.createdAt)} -
+
+

Additional Details

+
+ {#if lead.rating} +
+

Rating

+ + {#each Array(parseInt(lead.rating) || 0) as _, i} + + {/each} + {#each Array(5 - (parseInt(lead.rating) || 0)) as _, i} + + {/each} + +
+ {/if} + {#if lead.annualRevenue} +
+

Annual Revenue

+

${lead.annualRevenue.toLocaleString()}

+
+ {/if} + {#if lead.address} +
+

Address

+

{lead.address}

+
+ {/if} +
+
+ +
+
+
+

Status

+

{lead.status}

+
+
+

Lead Owner

+

{lead.owner?.name || 'Unassigned'}

+
+
+

Created At

+

{formatDate(lead.createdAt)}

+
+
+

Updated At

+

{formatDate(lead.updatedAt)}

+
+ {#if lead.isConverted && lead.convertedAt} +
+

Converted At

+

{formatDate(lead.convertedAt)}

+
+ {/if} +
+
-
-
-
- Updated - {formatDate(lead.updatedAt)} -
+
+ {@html lead.description}
- {#if lead.isConverted && lead.convertedAt} -
-
-
- Converted - {formatDate(lead.convertedAt)} -
-
- {/if} -
-
- +
+
- - -

Lead Owner

-
-
- - - -
-
-
{lead.owner?.name || 'Unassigned'}
- {#if lead.owner?.email} -
{lead.owner.email}
- {/if} + +