Skip to content

Commit 2a8c8ae

Browse files
authored
Merge pull request #22 from MicroPyramid/dev
Dev
2 parents a927657 + e3042fb commit 2a8c8ae

Some content is hidden

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

43 files changed

+5676
-2516
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- DropForeignKey
2+
ALTER TABLE "Contact" DROP CONSTRAINT "Contact_organizationId_fkey";
3+
4+
-- AlterTable
5+
ALTER TABLE "Contact" ALTER COLUMN "organizationId" DROP NOT NULL;
6+
7+
-- AddForeignKey
8+
ALTER TABLE "Contact" ADD CONSTRAINT "Contact_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- CreateEnum
2+
CREATE TYPE "OpportunityStatus" AS ENUM ('SUCCESS', 'FAILED', 'IN_PROGRESS');
3+
4+
-- AlterTable
5+
ALTER TABLE "Opportunity" ADD COLUMN "status" "OpportunityStatus";

prisma/schema.prisma

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,16 @@ model Contact {
155155
updatedAt DateTime @updatedAt
156156
owner User @relation(fields: [ownerId], references: [id])
157157
ownerId String
158-
organization Organization @relation(fields: [organizationId], references: [id])
159-
organizationId String
158+
organization Organization? @relation(fields: [organizationId], references: [id])
159+
organizationId String?
160160
tasks Task[]
161161
events Event[]
162162
opportunities Opportunity[]
163163
cases Case[]
164164
leads Lead[] // Keep this relation for historical tracking only
165165
comments Comment[]
166166
quotes Quote[] @relation("ContactQuotes")
167-
relatedAccounts AccountContactRelationship[]
167+
relatedAccounts AccountContactRelationship[]
168168
}
169169

170170
enum LeadSource {
@@ -217,11 +217,17 @@ enum LeadStatus {
217217
UNQUALIFIED
218218
CONVERTED
219219
}
220+
enum OpportunityStatus {
221+
SUCCESS
222+
FAILED
223+
IN_PROGRESS
224+
}
220225

221226
model Opportunity {
222227
id String @id @default(uuid())
223228
name String
224229
amount Float?
230+
status OpportunityStatus?
225231
closeDate DateTime?
226232
probability Float? // Percentage chance to close (0-100)
227233
type String? // New Business, Existing Business, etc.
@@ -647,17 +653,17 @@ enum ContentBlockType {
647653
}
648654

649655
model NewsletterSubscriber {
650-
id String @id @default(uuid())
651-
email String @unique
652-
isActive Boolean @default(true)
653-
subscribedAt DateTime @default(now())
654-
unsubscribedAt DateTime?
655-
confirmationToken String? @unique
656-
isConfirmed Boolean @default(false)
657-
confirmedAt DateTime?
658-
ipAddress String?
659-
userAgent String?
660-
656+
id String @id @default(uuid())
657+
email String @unique
658+
isActive Boolean @default(true)
659+
subscribedAt DateTime @default(now())
660+
unsubscribedAt DateTime?
661+
confirmationToken String? @unique
662+
isConfirmed Boolean @default(false)
663+
confirmedAt DateTime?
664+
ipAddress String?
665+
userAgent String?
666+
661667
@@index([email])
662668
@@index([isActive])
663669
@@index([subscribedAt])

src/routes/(app)/Sidebar.svelte

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,29 @@
7676
]
7777
},
7878
{
79-
key: 'accounts',
79+
key: 'contacts',
80+
label: 'Contacts',
81+
icon: Users,
82+
type: 'dropdown',
83+
children: [
84+
{ href: '/app/contacts', label: 'All Contacts', icon: List },
85+
{ href: '/app/contacts/new', label: 'New Contact', icon: UserPlus }
86+
]
87+
},
88+
{
89+
href: '/app/accounts',
8090
label: 'Accounts',
8191
icon: Building,
92+
type: 'link'
93+
},
94+
{
95+
key: 'opportunities',
96+
label: 'Opportunities',
97+
icon: Target,
8298
type: 'dropdown',
8399
children: [
84-
{ href: '/app/accounts', label: 'All Accounts', icon: List },
85-
{ href: '/app/accounts/new', label: 'New Account', icon: Plus },
86-
{ href: '/app/accounts/opportunities', label: 'Opportunities', icon: Target }
100+
{ href: '/app/opportunities', label: 'All Opportunities', icon: List },
101+
{ href: '/app/opportunities/new', label: 'New Opportunity', icon: Plus }
87102
]
88103
},
89104
{

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export async function load({ locals, url, params }) {
1919
const status = url.searchParams.get('status');
2020
if (status === 'open') {
2121
where.closedAt = null;
22+
where.active = true;
2223
} else if (status === 'closed') {
2324
where.closedAt = { not: null };
2425
}
@@ -73,6 +74,7 @@ export async function load({ locals, url, params }) {
7374
return {
7475
accounts: accounts.map(account => ({
7576
...account,
77+
isActive: account.isActive, // Use only the active field, ignore closedAt for display purposes
7678
opportunityCount: account.opportunities.length,
7779
contactCount: account.relatedContacts.length,
7880
taskCount: account.tasks.length,

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
</div>
154154
<div>
155155
<p class="text-sm text-green-600 dark:text-green-400 font-medium">Active</p>
156-
<p class="text-2xl font-bold text-green-900 dark:text-green-100">{accounts.filter(a => !a.closedAt).length}</p>
156+
<p class="text-2xl font-bold text-green-900 dark:text-green-100">{accounts.filter(a => a.isActive).length}</p>
157157
</div>
158158
</div>
159159
</div>
@@ -252,14 +252,21 @@
252252
<p class="text-sm font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
253253
{account.name}
254254
</p>
255-
{#if account.closedAt}
256-
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400 mt-1">
257-
Closed
258-
</span>
259-
{:else}
255+
{#if account.isActive}
260256
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400 mt-1">
261257
Active
262258
</span>
259+
{:else}
260+
<div class="mt-1">
261+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400">
262+
Closed
263+
</span>
264+
{#if account.closedAt}
265+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
266+
{formatDate(account.closedAt)}
267+
</p>
268+
{/if}
269+
</div>
263270
{/if}
264271
</a>
265272
</div>
@@ -335,16 +342,16 @@
335342
title="View Account">
336343
<Eye class="w-4 h-4" />
337344
</a>
345+
<a href="/app/opportunities/new?accountId={account.id}"
346+
class="p-2 text-gray-400 hover:text-green-600 dark:hover:text-green-400 transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
347+
title="Add Opportunity">
348+
<Plus class="w-4 h-4" />
349+
</a>
338350
<a href="/app/accounts/{account.id}/edit"
339351
class="p-2 text-gray-400 hover:text-yellow-600 dark:hover:text-yellow-400 transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
340352
title="Edit Account">
341353
<Edit class="w-4 h-4" />
342354
</a>
343-
<a href="/app/accounts/{account.id}/delete"
344-
class="p-2 text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
345-
title="Delete Account">
346-
<Trash2 class="w-4 h-4" />
347-
</a>
348355
</div>
349356
</td>
350357
</tr>

src/routes/(app)/app/accounts/[accountId]/+page.server.js

Lines changed: 5 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export async function load({ params, url, locals }) {
127127

128128
/** @type {import('./$types').Actions} */
129129
export const actions = {
130-
closeAccount: async ({ params, request, locals }) => {
130+
closeAccount: async ({ request, locals, params }) => {
131131
try {
132132
const user = locals.user;
133133
const org = locals.org;
@@ -180,6 +180,7 @@ export const actions = {
180180
where: { id: accountId },
181181
data: {
182182
closedAt: new Date(),
183+
isActive: false,
183184
closureReason
184185
}
185186
});
@@ -206,7 +207,7 @@ export const actions = {
206207
}
207208
},
208209

209-
reopenAccount: async ({ params, request, locals }) => {
210+
reopenAccount: async ({ locals, params }) => {
210211
try {
211212
const user = locals.user;
212213
const org = locals.org;
@@ -260,6 +261,7 @@ export const actions = {
260261
where: { id: accountId },
261262
data: {
262263
closedAt: null,
264+
isActive: true,
263265
closureReason: null
264266
}
265267
});
@@ -286,111 +288,7 @@ export const actions = {
286288
}
287289
},
288290

289-
addContact: async ({ params, request, locals }) => {
290-
try {
291-
const user = locals.user;
292-
const org = locals.org;
293-
294-
const { accountId } = params;
295-
let data;
296-
// Support both JSON and form submissions
297-
const contentType = request.headers.get('content-type') || '';
298-
if (contentType.includes('application/json')) {
299-
data = await request.json();
300-
} else {
301-
const formData = await request.formData();
302-
data = Object.fromEntries(formData.entries());
303-
}
304-
const firstName = data.firstName?.toString().trim();
305-
const lastName = data.lastName?.toString().trim();
306-
if (!firstName || !lastName) {
307-
return fail(400, { success: false, message: 'First and last name are required.' });
308-
}
309-
310-
// check if the account exists and belongs to the organization
311-
const account = await prisma.account.findUnique({
312-
where: { id: accountId, organizationId: org.id }
313-
});
314-
if (!account) {
315-
return fail(404, { success: false, message: 'Account not found or does not belong to this organization.' });
316-
}
317-
// Create the contact
318-
const contact = await prisma.contact.create({
319-
data: {
320-
firstName,
321-
lastName,
322-
email: data.email?.toString() || null,
323-
phone: data.phone?.toString() || null,
324-
title: data.title?.toString() || null,
325-
ownerId: user.id,
326-
organizationId: org.id,
327-
}
328-
});
329-
// Link contact to account
330-
await prisma.accountContactRelationship.create({
331-
data: {
332-
accountId,
333-
contactId: contact.id,
334-
isPrimary: !!data.isPrimary,
335-
role: data.role?.toString() || null
336-
}
337-
});
338-
return { success: true, message: 'Contact added successfully.' };
339-
} catch (err) {
340-
console.error('Error adding contact:', err);
341-
return fail(500, { success: false, message: 'Failed to add contact.' });
342-
}
343-
},
344-
345-
addOpportunity: async ({ params, request, locals }) => {
346-
try {
347-
const user = locals.user;
348-
const org = locals.org;
349-
350-
const { accountId } = params;
351-
const formData = await request.formData();
352-
const name = formData.get('name')?.toString().trim();
353-
const amountRaw = formData.get('amount');
354-
const amount = amountRaw ? parseFloat(amountRaw.toString()) : null;
355-
const stageRaw = formData.get('stage');
356-
const stage = stageRaw ? stageRaw.toString() : 'PROSPECTING';
357-
const closeDateRaw = formData.get('closeDate');
358-
const closeDate = closeDateRaw ? new Date(closeDateRaw.toString()) : null;
359-
const probabilityRaw = formData.get('probability');
360-
const probability = probabilityRaw ? parseFloat(probabilityRaw.toString()) : null;
361-
if (!name) {
362-
return fail(400, { success: false, message: 'Opportunity name is required.' });
363-
}
364-
365-
// chek if the account exists and belongs to the organization
366-
const account = await prisma.account.findUnique({
367-
where: { id: accountId, organizationId: org.id }
368-
});
369-
if (!account) {
370-
return fail(404, { success: false, message: 'Account not found or does not belong to this organization.' });
371-
}
372-
373-
// Create the opportunity
374-
await prisma.opportunity.create({
375-
data: {
376-
name,
377-
amount,
378-
stage,
379-
closeDate,
380-
probability,
381-
account: { connect: { id: accountId } },
382-
owner: { connect: { id: user.id } },
383-
organization: { connect: { id: org.id } }
384-
}
385-
});
386-
return { success: true, message: 'Opportunity added successfully.' };
387-
} catch (err) {
388-
console.error('Error adding opportunity:', err);
389-
return fail(500, { success: false, message: 'Failed to add opportunity.' });
390-
}
391-
},
392-
393-
comment: async ({ request, params, locals }) => {
291+
comment: async ({ request, locals, params }) => {
394292
const user = locals.user;
395293
const org = locals.org;
396294
// Fallback: fetch account to get organizationId
@@ -416,51 +314,5 @@ export const actions = {
416314
}
417315
});
418316
return { success: true };
419-
},
420-
421-
addTask: async ({ params, request, locals }) => {
422-
try {
423-
const user = locals.user;
424-
const org = locals.org;
425-
426-
const { accountId } = params;
427-
const formData = await request.formData();
428-
const subject = formData.get('subject')?.toString().trim();
429-
const description = formData.get('description')?.toString() || '';
430-
const dueDateRaw = formData.get('dueDate');
431-
const dueDate = dueDateRaw ? new Date(dueDateRaw.toString()) : null;
432-
const priority = formData.get('priority')?.toString() || 'Normal';
433-
if (!subject) {
434-
return fail(400, { success: false, message: 'Subject is required.' });
435-
}
436-
437-
// Check if the account exists and belongs to the organization
438-
const account = await prisma.account.findUnique({
439-
where: { id: accountId, organizationId: org.id }
440-
});
441-
if (!account) {
442-
return fail(404, { success: false, message: 'Account not found or does not belong to this organization.' });
443-
}
444-
// If no ownerId is provided, default to current user
445-
// if (!ownerId) ownerId = user.id;
446-
// console.log(user.id, org.id);
447-
const task = await prisma.task.create({
448-
data: {
449-
subject,
450-
description,
451-
dueDate,
452-
priority,
453-
status: 'Open',
454-
createdBy: { connect: { id: user.id } },
455-
account: { connect: { id: accountId } },
456-
owner: { connect: { id: user.id } },
457-
organization: { connect: { id: org.id } },
458-
}
459-
});
460-
return { success: true, message: 'Task added successfully.', task };
461-
} catch (err) {
462-
console.error('Error adding task:', err);
463-
return fail(500, { success: false, message: 'Failed to add task.' });
464-
}
465317
}
466318
};

0 commit comments

Comments
 (0)