Skip to content

Commit 1542d77

Browse files
committed
feat: Add type annotations and improve data handling across various components
- Added type annotations for `data` and `form` in multiple Svelte components to enhance type safety. - Improved handling of optional chaining and default values for various properties in forms and data structures. - Refactored date formatting functions to ensure consistent handling of date inputs. - Enhanced validation logic for form submissions, ensuring required fields are checked and valid enum values are enforced. - Updated opportunity closing logic to handle potential null values and ensure correct data types are used. - Improved user role management in the users page, simplifying role handling and ensuring only relevant roles are displayed. - General code cleanup and organization for better readability and maintainability.
1 parent a5ba51d commit 1542d77

File tree

17 files changed

+300
-130
lines changed

17 files changed

+300
-130
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Plus
1414
} from '@lucide/svelte';
1515
16+
/** @type {any} */
1617
export let data;
1718
1819
$: metrics = data.metrics || {};

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
Send
2828
} from '@lucide/svelte';
2929
30+
/** @type {any} */
3031
export let data;
32+
/** @type {any} */
3133
export let form;
3234
let users = Array.isArray(data.users) ? data.users : [];
3335
34-
const { account, contacts, opportunities, quotes, tasks, cases } = data;
36+
const { account, contacts, opportunities = [], quotes, tasks, cases } = data;
3537
let comments = data.comments;
3638
3739
// Form state

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
let sortDirection = $state('desc');
3535
let showFilters = $state(false);
3636
let showDeleteModal = $state(false);
37+
/** @type {any} */
3738
let opportunityToDelete = $state(null);
3839
let deleteLoading = $state(false);
3940
@@ -81,6 +82,10 @@
8182
const filteredOpportunities = $derived(getFilteredOpportunities());
8283
8384
85+
/**
86+
* @param {number | null} amount
87+
* @returns {string}
88+
*/
8489
function formatCurrency(amount) {
8590
if (!amount) return '-';
8691
return new Intl.NumberFormat('en-US', {
@@ -91,6 +96,10 @@
9196
}).format(amount);
9297
}
9398
99+
/**
100+
* @param {string | Date | null} date
101+
* @returns {string}
102+
*/
94103
function formatDate(date) {
95104
if (!date) return '-';
96105
return new Date(date).toLocaleDateString('en-US', {
@@ -100,6 +109,9 @@
100109
});
101110
}
102111
112+
/**
113+
* @param {string} field
114+
*/
103115
function toggleSort(field) {
104116
if (sortField === field) {
105117
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
@@ -109,6 +121,9 @@
109121
}
110122
}
111123
124+
/**
125+
* @param {any} opportunity
126+
*/
112127
function openDeleteModal(opportunity) {
113128
opportunityToDelete = opportunity;
114129
showDeleteModal = true;

src/routes/(app)/app/opportunities/[opportunityId]/+page.svelte

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,40 @@
3232
'CLOSED_LOST': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
3333
};
3434
35-
const getStageColor = (stage) => stageColors[stage] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
35+
/**
36+
* @param {string} stage
37+
* @returns {string}
38+
*/
39+
const getStageColor = (stage) => stageColors[/** @type {keyof typeof stageColors} */ (stage)] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
3640
41+
/**
42+
* @param {number | null} amount
43+
* @returns {string}
44+
*/
3745
const formatCurrency = (amount) => {
3846
return amount ? `$${amount.toLocaleString()}` : 'N/A';
3947
};
4048
49+
/**
50+
* @param {string | Date | null} date
51+
* @returns {string}
52+
*/
4153
const formatDate = (date) => {
4254
return date ? new Date(date).toLocaleDateString() : 'N/A';
4355
};
4456
57+
/**
58+
* @param {string | Date | null} date
59+
* @returns {string}
60+
*/
4561
const formatDateTime = (date) => {
4662
return date ? new Date(date).toLocaleString() : 'N/A';
4763
};
4864
65+
/**
66+
* @param {string} stage
67+
* @returns {number}
68+
*/
4969
const getStageProgress = (stage) => {
5070
const stages = ['PROSPECTING', 'QUALIFICATION', 'PROPOSAL', 'NEGOTIATION', 'CLOSED_WON'];
5171
const index = stages.indexOf(stage);
@@ -212,7 +232,7 @@
212232
<div class="flex items-center justify-between">
213233
<span class="text-sm text-gray-500 dark:text-gray-400">Days to Close</span>
214234
<span class="font-semibold text-gray-900 dark:text-white">
215-
{opportunity.closeDate ? Math.ceil((new Date(opportunity.closeDate) - new Date()) / (1000 * 60 * 60 * 24)) : 'N/A'}
235+
{opportunity.closeDate ? Math.ceil((new Date(opportunity.closeDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) : 'N/A'}
216236
</span>
217237
</div>
218238
</div>

src/routes/(app)/app/opportunities/[opportunityId]/close/+page.server.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { error, fail, redirect } from '@sveltejs/kit';
22
import prisma from '$lib/prisma';
33

4+
/**
5+
* @param {Object} options
6+
* @param {Record<string, string>} options.params
7+
* @param {App.Locals} options.locals
8+
*/
49
export async function load({ params, locals }) {
510
if (!locals.org?.id) {
611
throw error(403, 'Organization access required');
@@ -26,23 +31,30 @@ export async function load({ params, locals }) {
2631
}
2732

2833
export const actions = {
34+
/**
35+
* @param {Object} options
36+
* @param {Request} options.request
37+
* @param {Record<string, string>} options.params
38+
* @param {App.Locals} options.locals
39+
*/
2940
default: async ({ request, params, locals }) => {
3041
if (!locals.org?.id) {
3142
return fail(403, { error: 'Organization access required' });
3243
}
3344

3445
const formData = await request.formData();
35-
const status = formData.get('status');
36-
const closeDate = formData.get('closeDate');
37-
const closeReason = formData.get('closeReason');
46+
const status = formData.get('status')?.toString();
47+
const closeDate = formData.get('closeDate')?.toString();
48+
const closeReason = formData.get('closeReason')?.toString();
3849

3950
// Validate required fields
4051
if (!status || !closeDate) {
4152
return fail(400, { error: 'Status and close date are required' });
4253
}
4354

4455
// Validate status
45-
if (!['CLOSED_WON', 'CLOSED_LOST'].includes(status)) {
56+
const validCloseStatuses = ['CLOSED_WON', 'CLOSED_LOST'];
57+
if (!status || !validCloseStatuses.includes(status)) {
4658
return fail(400, { error: 'Invalid status selected' });
4759
}
4860

@@ -63,12 +75,14 @@ export const actions = {
6375
}
6476

6577
// Update the opportunity with closing details
66-
const updatedOpportunity = await prisma.opportunity.update({
78+
const opportunityStage = /** @type {import('@prisma/client').OpportunityStage} */ (status);
79+
80+
await prisma.opportunity.update({
6781
where: { id: params.opportunityId },
6882
data: {
69-
stage: status, // CLOSED_WON or CLOSED_LOST
83+
stage: opportunityStage, // CLOSED_WON or CLOSED_LOST
7084
status: status === 'CLOSED_WON' ? 'SUCCESS' : 'FAILED',
71-
closeDate: new Date(closeDate),
85+
closeDate: closeDate ? new Date(closeDate) : null,
7286
description: closeReason ?
7387
(opportunity.description ? `${opportunity.description}\n\nClose Reason: ${closeReason}` : `Close Reason: ${closeReason}`)
7488
: opportunity.description,
@@ -97,7 +111,7 @@ export const actions = {
97111
throw redirect(303, `/app/opportunities/${opportunity.id}`);
98112
} catch (err) {
99113
console.error('Error closing opportunity:', err);
100-
if (err.status === 303) {
114+
if (err && typeof err === 'object' && 'status' in err && err.status === 303) {
101115
throw err; // Re-throw redirect
102116
}
103117
return fail(500, { error: 'Failed to close opportunity. Please try again.' });

src/routes/(app)/app/opportunities/[opportunityId]/close/+page.svelte

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,6 @@
1616
{ value: 'CLOSED_LOST', label: 'Closed Lost', color: 'text-red-600' }
1717
];
1818
19-
function handleSubmit() {
20-
return async ({ update }) => {
21-
isSubmitting = true;
22-
await update();
23-
isSubmitting = false;
24-
};
25-
}
2619
</script>
2720

2821
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
@@ -77,7 +70,13 @@
7770
<h2 class="text-lg font-medium text-gray-900 dark:text-white">Close Opportunity</h2>
7871
</div>
7972

80-
<form method="POST" use:enhance={handleSubmit} class="p-6 space-y-6">
73+
<form method="POST" use:enhance={() => {
74+
return async ({ update }) => {
75+
isSubmitting = true;
76+
await update();
77+
isSubmitting = false;
78+
};
79+
}} class="p-6 space-y-6">
8180
{#if form?.error}
8281
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
8382
<div class="flex items-center gap-2">

src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.server.js

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import { error, fail, redirect } from '@sveltejs/kit';
22
import prisma from '$lib/prisma';
33

4-
export async function load({ params }) {
5-
const opportunity = await prisma.opportunity.findUnique({
6-
where: { id: params.opportunityId },
4+
/**
5+
* @param {Object} options
6+
* @param {Record<string, string>} options.params
7+
* @param {App.Locals} options.locals
8+
*/
9+
export async function load({ params, locals }) {
10+
if (!locals.org?.id) {
11+
throw error(403, 'Organization access required');
12+
}
13+
14+
const opportunity = await prisma.opportunity.findFirst({
15+
where: {
16+
id: params.opportunityId,
17+
organizationId: locals.org.id
18+
},
719
include: {
820
account: {
921
select: {
@@ -28,15 +40,26 @@ export async function load({ params }) {
2840
}
2941

3042
export const actions = {
31-
default: async ({ request, params }) => {
43+
/**
44+
* @param {Object} options
45+
* @param {Request} options.request
46+
* @param {Record<string, string>} options.params
47+
* @param {App.Locals} options.locals
48+
*/
49+
default: async ({ request, params, locals }) => {
50+
if (!locals.org?.id) {
51+
return fail(403, { error: 'Organization access required' });
52+
}
53+
3254
const form = await request.formData();
3355

3456
const name = form.get('name')?.toString().trim();
35-
const amount = form.get('amount') ? parseFloat(form.get('amount')) : null;
36-
const expectedRevenue = form.get('expectedRevenue') ? parseFloat(form.get('expectedRevenue')) : null;
57+
const amount = form.get('amount') ? parseFloat(form.get('amount')?.toString() || '') : null;
58+
const expectedRevenue = form.get('expectedRevenue') ? parseFloat(form.get('expectedRevenue')?.toString() || '') : null;
3759
const stage = form.get('stage')?.toString();
38-
const probability = form.get('probability') ? parseFloat(form.get('probability')) : null;
39-
const closeDate = form.get('closeDate') ? new Date(form.get('closeDate')) : null;
60+
const probability = form.get('probability') ? parseFloat(form.get('probability')?.toString() || '') : null;
61+
const closeDateValue = form.get('closeDate')?.toString();
62+
const closeDate = closeDateValue ? new Date(closeDateValue) : null;
4063
const leadSource = form.get('leadSource')?.toString() || null;
4164
const forecastCategory = form.get('forecastCategory')?.toString() || null;
4265
const type = form.get('type')?.toString() || null;
@@ -51,6 +74,12 @@ export const actions = {
5174
return fail(400, { message: 'Stage is required.' });
5275
}
5376

77+
// Validate stage is a valid enum value
78+
const validStages = ['PROSPECTING', 'QUALIFICATION', 'PROPOSAL', 'NEGOTIATION', 'CLOSED_WON', 'CLOSED_LOST'];
79+
if (!validStages.includes(stage)) {
80+
return fail(400, { message: 'Invalid stage selected.' });
81+
}
82+
5483
// Validate probability range
5584
if (probability !== null && (probability < 0 || probability > 100)) {
5685
return fail(400, { message: 'Probability must be between 0 and 100.' });
@@ -66,13 +95,27 @@ export const actions = {
6695
}
6796

6897
try {
98+
// Verify the opportunity exists and belongs to the organization
99+
const existingOpportunity = await prisma.opportunity.findFirst({
100+
where: {
101+
id: params.opportunityId,
102+
organizationId: locals.org.id
103+
}
104+
});
105+
106+
if (!existingOpportunity) {
107+
return fail(404, { message: 'Opportunity not found' });
108+
}
109+
110+
const opportunityStage = /** @type {import('@prisma/client').OpportunityStage} */ (stage);
111+
69112
await prisma.opportunity.update({
70113
where: { id: params.opportunityId },
71114
data: {
72115
name,
73116
amount,
74117
expectedRevenue,
75-
stage,
118+
stage: opportunityStage,
76119
probability,
77120
closeDate,
78121
leadSource,

src/routes/(app)/app/opportunities/[opportunityId]/edit/+page.svelte

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,24 @@
1010
? new Date(opportunity.closeDate).toISOString().slice(0, 10)
1111
: '';
1212
13-
$: opportunity.closeDate = closeDateStr;
13+
$: {
14+
if (closeDateStr) {
15+
opportunity.closeDate = new Date(closeDateStr);
16+
} else {
17+
opportunity.closeDate = null;
18+
}
19+
}
1420
21+
/**
22+
* @param {SubmitEvent} e
23+
*/
1524
async function handleSubmit(e) {
1625
e.preventDefault();
1726
isSubmitting = true;
1827
error = '';
1928
20-
const formData = new FormData(e.target);
29+
const target = /** @type {HTMLFormElement} */ (e.target);
30+
const formData = new FormData(target);
2131
const res = await fetch('', { method: 'POST', body: formData });
2232
2333
if (res.ok) {
@@ -307,8 +317,3 @@
307317
</div>
308318
</div>
309319
310-
<style>
311-
@media (max-width: 640px) {
312-
.max-w-xl { max-width: 100%; padding: 0.5rem; }
313-
}
314-
</style>

0 commit comments

Comments
 (0)