Skip to content

Commit d81a422

Browse files
authored
Merge pull request #65 from MicroPyramid/dev
Dev
2 parents 3814777 + 9f5d5ee commit d81a422

File tree

4 files changed

+576
-12
lines changed

4 files changed

+576
-12
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ Create a `.env` file based on the following template:
9191
# Database Configuration
9292
DATABASE_URL="postgresql://postgres:password@localhost:5432/bottlecrm?schema=public"
9393
94+
# JWT Secret (required for authentication)
95+
# Generate a secure secret using openssl:
96+
# openssl rand -base64 32
97+
JWT_SECRET="<your-generated-secret>"
98+
9499
# Google OAuth (Optional)
95100
GOOGLE_CLIENT_ID=""
96101
GOOGLE_CLIENT_SECRET=""

api/routes/leads.js

Lines changed: 178 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,39 @@ router.get('/metadata', async (req, res) => {
159159
* schema:
160160
* type: integer
161161
* default: 10
162+
* - in: query
163+
* name: search
164+
* schema:
165+
* type: string
166+
* description: Search by name, email, or company
167+
* - in: query
168+
* name: status
169+
* schema:
170+
* type: string
171+
* enum: [NEW, PENDING, CONTACTED, QUALIFIED, UNQUALIFIED, CONVERTED]
172+
* description: Filter by lead status
173+
* - in: query
174+
* name: leadSource
175+
* schema:
176+
* type: string
177+
* enum: [WEB, PHONE_INQUIRY, PARTNER_REFERRAL, COLD_CALL, TRADE_SHOW, EMPLOYEE_REFERRAL, ADVERTISEMENT, OTHER]
178+
* description: Filter by lead source
179+
* - in: query
180+
* name: industry
181+
* schema:
182+
* type: string
183+
* description: Filter by industry
184+
* - in: query
185+
* name: rating
186+
* schema:
187+
* type: string
188+
* enum: [Hot, Warm, Cold]
189+
* description: Filter by rating
190+
* - in: query
191+
* name: converted
192+
* schema:
193+
* type: boolean
194+
* description: Filter by conversion status
162195
* responses:
163196
* 200:
164197
* description: List of leads
@@ -179,10 +212,82 @@ router.get('/', async (req, res) => {
179212
const page = parseInt(req.query.page) || 1;
180213
const limit = parseInt(req.query.limit) || 10;
181214
const skip = (page - 1) * limit;
215+
216+
const {
217+
search,
218+
status,
219+
leadSource,
220+
industry,
221+
rating,
222+
converted
223+
} = req.query;
224+
225+
// Build where clause for filtering
226+
let whereClause = {
227+
organizationId: req.organizationId
228+
};
229+
230+
// Add search filter (search in firstName, lastName, email, company)
231+
if (search) {
232+
whereClause.OR = [
233+
{
234+
firstName: {
235+
contains: search,
236+
mode: 'insensitive'
237+
}
238+
},
239+
{
240+
lastName: {
241+
contains: search,
242+
mode: 'insensitive'
243+
}
244+
},
245+
{
246+
email: {
247+
contains: search,
248+
mode: 'insensitive'
249+
}
250+
},
251+
{
252+
company: {
253+
contains: search,
254+
mode: 'insensitive'
255+
}
256+
}
257+
];
258+
}
259+
260+
// Add status filter
261+
if (status) {
262+
whereClause.status = status;
263+
}
264+
265+
// Add leadSource filter
266+
if (leadSource) {
267+
whereClause.leadSource = leadSource;
268+
}
269+
270+
// Add industry filter
271+
if (industry) {
272+
whereClause.industry = {
273+
contains: industry,
274+
mode: 'insensitive'
275+
};
276+
}
277+
278+
// Add rating filter
279+
if (rating) {
280+
whereClause.rating = rating;
281+
}
282+
283+
// Add converted filter
284+
if (converted !== undefined) {
285+
whereClause.isConverted = converted === 'true';
286+
}
182287

183288
const [leads, total] = await Promise.all([
184289
prisma.lead.findMany({
185-
where: { organizationId: req.organizationId },
290+
where: whereClause,
186291
skip,
187292
take: limit,
188293
orderBy: { createdAt: 'desc' },
@@ -193,17 +298,25 @@ router.get('/', async (req, res) => {
193298
}
194299
}),
195300
prisma.lead.count({
196-
where: { organizationId: req.organizationId }
301+
where: whereClause
197302
})
198303
]);
199304

305+
// Calculate pagination info
306+
const totalPages = Math.ceil(total / limit);
307+
const hasNext = page < totalPages;
308+
const hasPrev = page > 1;
309+
200310
res.json({
311+
success: true,
201312
leads,
202313
pagination: {
203314
page,
204315
limit,
205316
total,
206-
pages: Math.ceil(total / limit)
317+
totalPages,
318+
hasNext,
319+
hasPrev
207320
}
208321
});
209322
} catch (error) {
@@ -297,10 +410,21 @@ router.get('/:id', async (req, res) => {
297410
* type: string
298411
* company:
299412
* type: string
413+
* title:
414+
* type: string
300415
* status:
301416
* type: string
417+
* enum: [NEW, PENDING, CONTACTED, QUALIFIED, UNQUALIFIED, CONVERTED]
302418
* leadSource:
303419
* type: string
420+
* enum: [WEB, PHONE_INQUIRY, PARTNER_REFERRAL, COLD_CALL, TRADE_SHOW, EMPLOYEE_REFERRAL, ADVERTISEMENT, OTHER]
421+
* industry:
422+
* type: string
423+
* rating:
424+
* type: string
425+
* enum: [Hot, Warm, Cold]
426+
* description:
427+
* type: string
304428
* responses:
305429
* 201:
306430
* description: Lead created successfully
@@ -309,21 +433,61 @@ router.get('/:id', async (req, res) => {
309433
*/
310434
router.post('/', async (req, res) => {
311435
try {
312-
const { firstName, lastName, email, phone, company, status, leadSource } = req.body;
436+
const {
437+
firstName,
438+
lastName,
439+
email,
440+
phone,
441+
company,
442+
title,
443+
status,
444+
leadSource,
445+
industry,
446+
rating,
447+
description
448+
} = req.body;
313449

314450
if (!firstName || !lastName || !email) {
315451
return res.status(400).json({ error: 'First name, last name, and email are required' });
316452
}
317453

454+
// Validate email format
455+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
456+
if (!emailRegex.test(email)) {
457+
return res.status(400).json({ error: 'Invalid email format' });
458+
}
459+
460+
// Validate status if provided
461+
const validStatuses = ['NEW', 'PENDING', 'CONTACTED', 'QUALIFIED', 'UNQUALIFIED', 'CONVERTED'];
462+
if (status && !validStatuses.includes(status)) {
463+
return res.status(400).json({ error: 'Invalid status value' });
464+
}
465+
466+
// Validate leadSource if provided
467+
const validSources = ['WEB', 'PHONE_INQUIRY', 'PARTNER_REFERRAL', 'COLD_CALL', 'TRADE_SHOW', 'EMPLOYEE_REFERRAL', 'ADVERTISEMENT', 'OTHER'];
468+
if (leadSource && !validSources.includes(leadSource)) {
469+
return res.status(400).json({ error: 'Invalid lead source value' });
470+
}
471+
472+
// Validate rating if provided
473+
const validRatings = ['Hot', 'Warm', 'Cold'];
474+
if (rating && !validRatings.includes(rating)) {
475+
return res.status(400).json({ error: 'Invalid rating value' });
476+
}
477+
318478
const lead = await prisma.lead.create({
319479
data: {
320-
firstName,
321-
lastName,
322-
email,
323-
phone,
324-
company,
325-
status: status || 'NEW',
326-
leadSource,
480+
firstName: firstName.trim(),
481+
lastName: lastName.trim(),
482+
email: email.trim().toLowerCase(),
483+
phone: phone?.trim() || null,
484+
company: company?.trim() || null,
485+
title: title?.trim() || null,
486+
status: status || 'PENDING',
487+
leadSource: leadSource || null,
488+
industry: industry?.trim() || null,
489+
rating: rating || null,
490+
description: description?.trim() || null,
327491
organizationId: req.organizationId,
328492
ownerId: req.userId
329493
},
@@ -337,6 +501,9 @@ router.post('/', async (req, res) => {
337501
res.status(201).json(lead);
338502
} catch (error) {
339503
console.error('Create lead error:', error);
504+
if (error.code === 'P2002') {
505+
return res.status(409).json({ error: 'A lead with this email already exists in this organization' });
506+
}
340507
res.status(500).json({ error: 'Internal server error' });
341508
}
342509
});

0 commit comments

Comments
 (0)