Skip to content

Commit 9d47e04

Browse files
authored
Merge pull request #66 from MicroPyramid/dev
Dev
2 parents d81a422 + 1542d77 commit 9d47e04

File tree

41 files changed

+636
-201
lines changed

Some content is hidden

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

41 files changed

+636
-201
lines changed

.github/copilot-instructions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ BottleCRM is a modern CRM application built with:
2323
## Important Notes
2424
- We need to ensure access control is strictly enforced based on user roles.
2525
- No record should be accessible unless the user or the org has the appropriate permissions.
26-
- When implementing forms in sveltekit A form label must be associated with a control
26+
- When implementing forms in sveltekit A form label must be associated with a control
27+
- svelte 5+ style coding standards should be followed

CLAUDE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ BottleCRM is a SaaS CRM platform built with SvelteKit, designed for startups and
1414
- **Icons**: Lucide Svelte
1515
- **Validation**: Zod
1616
- **Package Manager**: pnpm
17+
- **Type Checking**: JSDoc style type annotations (no TypeScript)
1718

1819
## Development Commands
1920

@@ -91,6 +92,42 @@ npx prisma studio
9192
- Use Zod for form validation
9293
- Follow existing patterns in `/contacts`, `/leads`, `/accounts` for consistency
9394

95+
## Coding Standards
96+
97+
### Type Safety
98+
- **NO TypeScript**: This project uses JavaScript with JSDoc style type annotations only
99+
- **JSDoc Comments**: Use JSDoc syntax for type information and documentation
100+
- **Type Checking**: Use `pnpm run check` to validate types via JSDoc annotations
101+
- **Function Parameters**: Document parameter types using JSDoc `@param` tags
102+
- **Return Types**: Document return types using JSDoc `@returns` tags
103+
104+
### JSDoc Examples
105+
```javascript
106+
/**
107+
* Updates a contact in the database
108+
* @param {string} contactId - The contact identifier
109+
* @param {Object} updateData - The data to update
110+
* @param {string} updateData.name - Contact name
111+
* @param {string} updateData.email - Contact email
112+
* @param {string} organizationId - Organization ID for data isolation
113+
* @returns {Promise<Object>} The updated contact object
114+
*/
115+
async function updateContact(contactId, updateData, organizationId) {
116+
// Implementation
117+
}
118+
119+
/**
120+
* @typedef {Object} User
121+
* @property {string} id - User ID
122+
* @property {string} email - User email
123+
* @property {string} name - User name
124+
* @property {string[]} organizationIds - Array of organization IDs
125+
*/
126+
127+
/** @type {User|null} */
128+
let currentUser = null;
129+
```
130+
94131
## Security Requirements
95132
- Never expose cross-organization data
96133
- Always filter queries by user's organization membership

src/app.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
gtag('config', 'G-JNWHD22PPN');
1212
</script>
1313
<meta charset="utf-8" />
14-
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
14+
<link rel="icon" href="%sveltekit.assets%/logo.png" />
1515
<meta name="viewport" content="width=device-width, initial-scale=1" />
1616
%sveltekit.head%
1717
</head>

src/lib/assets/images/logo.png

-17.8 KB
Loading

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">

0 commit comments

Comments
 (0)