add contacts to crm & display on dashboard
This commit is contained in:
@@ -15,6 +15,7 @@ import { apiRateLimiter } from './middlewares/security/rateLimiter.js';
|
|||||||
import authRoutes from './routes/auth.routes.js';
|
import authRoutes from './routes/auth.routes.js';
|
||||||
import adminRoutes from './routes/admin.routes.js';
|
import adminRoutes from './routes/admin.routes.js';
|
||||||
import contactRoutes from './routes/contact.routes.js';
|
import contactRoutes from './routes/contact.routes.js';
|
||||||
|
import personalContactRoutes from './routes/personal-contact.routes.js';
|
||||||
import crmEmailRoutes from './routes/crm-email.routes.js';
|
import crmEmailRoutes from './routes/crm-email.routes.js';
|
||||||
import emailAccountRoutes from './routes/email-account.routes.js';
|
import emailAccountRoutes from './routes/email-account.routes.js';
|
||||||
import timesheetRoutes from './routes/timesheet.routes.js';
|
import timesheetRoutes from './routes/timesheet.routes.js';
|
||||||
@@ -78,6 +79,7 @@ app.get('/health', (req, res) => {
|
|||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/admin', adminRoutes);
|
app.use('/api/admin', adminRoutes);
|
||||||
app.use('/api/contacts', contactRoutes);
|
app.use('/api/contacts', contactRoutes);
|
||||||
|
app.use('/api/personal-contacts', personalContactRoutes);
|
||||||
app.use('/api/emails', crmEmailRoutes);
|
app.use('/api/emails', crmEmailRoutes);
|
||||||
app.use('/api/email-accounts', emailAccountRoutes);
|
app.use('/api/email-accounts', emailAccountRoutes);
|
||||||
app.use('/api/timesheets', timesheetRoutes);
|
app.use('/api/timesheets', timesheetRoutes);
|
||||||
|
|||||||
59
src/controllers/personal-contact.controller.js
Normal file
59
src/controllers/personal-contact.controller.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as personalContactService from '../services/personal-contact.service.js'
|
||||||
|
import { BadRequestError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
const normalizePayload = (body) => ({
|
||||||
|
firstName: body.firstName?.trim(),
|
||||||
|
lastName: body.lastName?.trim() || null,
|
||||||
|
phone: body.phone?.trim(),
|
||||||
|
email: body.email?.trim(),
|
||||||
|
secondaryEmail: body.secondaryEmail?.trim() || null,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const listPersonalContacts = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const contacts = await personalContactService.listPersonalContacts(req.userId)
|
||||||
|
res.status(200).json({ success: true, data: contacts })
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPersonalContact = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const payload = normalizePayload(req.body)
|
||||||
|
if (!payload.firstName || !payload.phone || !payload.email) {
|
||||||
|
throw new BadRequestError('firstName, phone a email sú povinné')
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await personalContactService.createPersonalContact(req.userId, payload)
|
||||||
|
|
||||||
|
res.status(201).json({ success: true, data: created })
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updatePersonalContact = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const payload = normalizePayload(req.body)
|
||||||
|
|
||||||
|
const updated = await personalContactService.updatePersonalContact(
|
||||||
|
req.params.contactId,
|
||||||
|
req.userId,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ success: true, data: updated })
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deletePersonalContact = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const result = await personalContactService.deletePersonalContact(req.params.contactId, req.userId)
|
||||||
|
res.status(200).json({ success: true, message: result.message })
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,21 @@ export const contacts = pgTable('contacts', {
|
|||||||
accountEmailUnique: unique('account_email_unique').on(table.emailAccountId, table.email),
|
accountEmailUnique: unique('account_email_unique').on(table.emailAccountId, table.email),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Personal contacts - osobné kontakty používateľa (bez väzby na firmu alebo email account)
|
||||||
|
export const personalContacts = pgTable('personal_contacts', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
firstName: text('first_name').notNull(),
|
||||||
|
lastName: text('last_name'),
|
||||||
|
phone: text('phone').notNull(),
|
||||||
|
email: text('email').notNull(),
|
||||||
|
secondaryEmail: text('secondary_email'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
personalContactUniqueEmail: unique('personal_contact_user_email').on(table.userId, table.email),
|
||||||
|
}));
|
||||||
|
|
||||||
// Emails table - uložené emaily z JMAP
|
// Emails table - uložené emaily z JMAP
|
||||||
// Emaily patria k email accountu a sú zdieľané medzi všetkými používateľmi s prístupom
|
// Emaily patria k email accountu a sú zdieľané medzi všetkými používateľmi s prístupom
|
||||||
export const emails = pgTable('emails', {
|
export const emails = pgTable('emails', {
|
||||||
|
|||||||
40
src/routes/personal-contact.routes.js
Normal file
40
src/routes/personal-contact.routes.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { authenticate } from '../middlewares/auth/authMiddleware.js'
|
||||||
|
import { validateBody, validateParams } from '../middlewares/security/validateInput.js'
|
||||||
|
import * as personalContactController from '../controllers/personal-contact.controller.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
const createContactSchema = z.object({
|
||||||
|
firstName: z.string().min(1, 'Meno je povinné'),
|
||||||
|
lastName: z.string().optional(),
|
||||||
|
phone: z.string().min(3, 'Telefón je povinný'),
|
||||||
|
email: z.string().email('Neplatný email'),
|
||||||
|
secondaryEmail: z.union([z.string().email('Neplatný email'), z.literal('')]).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateContactSchema = createContactSchema.partial()
|
||||||
|
|
||||||
|
const contactIdSchema = z.object({ contactId: z.string().uuid() })
|
||||||
|
|
||||||
|
router.use(authenticate)
|
||||||
|
|
||||||
|
router.get('/', personalContactController.listPersonalContacts)
|
||||||
|
|
||||||
|
router.post('/', validateBody(createContactSchema), personalContactController.createPersonalContact)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/:contactId',
|
||||||
|
validateParams(contactIdSchema),
|
||||||
|
validateBody(updateContactSchema),
|
||||||
|
personalContactController.updatePersonalContact
|
||||||
|
)
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:contactId',
|
||||||
|
validateParams(contactIdSchema),
|
||||||
|
personalContactController.deletePersonalContact
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
104
src/services/personal-contact.service.js
Normal file
104
src/services/personal-contact.service.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { db } from '../config/database.js'
|
||||||
|
import { personalContacts } from '../db/schema.js'
|
||||||
|
import { eq, and, desc, ne } from 'drizzle-orm'
|
||||||
|
import { ConflictError, NotFoundError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
export const listPersonalContacts = async (userId) => {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(personalContacts)
|
||||||
|
.where(eq(personalContacts.userId, userId))
|
||||||
|
.orderBy(desc(personalContacts.updatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContactById = async (contactId, userId) => {
|
||||||
|
const [contact] = await db
|
||||||
|
.select()
|
||||||
|
.from(personalContacts)
|
||||||
|
.where(and(eq(personalContacts.id, contactId), eq(personalContacts.userId, userId)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!contact) {
|
||||||
|
throw new NotFoundError('Kontakt nenájdený')
|
||||||
|
}
|
||||||
|
|
||||||
|
return contact
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPersonalContact = async (userId, contact) => {
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: personalContacts.id })
|
||||||
|
.from(personalContacts)
|
||||||
|
.where(and(eq(personalContacts.userId, userId), eq(personalContacts.email, contact.email)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictError('Kontakt s týmto emailom už existuje')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [created] = await db
|
||||||
|
.insert(personalContacts)
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
firstName: contact.firstName,
|
||||||
|
lastName: contact.lastName || null,
|
||||||
|
phone: contact.phone,
|
||||||
|
email: contact.email,
|
||||||
|
secondaryEmail: contact.secondaryEmail || null,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return created
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updatePersonalContact = async (contactId, userId, updates) => {
|
||||||
|
await getContactById(contactId, userId)
|
||||||
|
|
||||||
|
if (updates.email) {
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: personalContacts.id })
|
||||||
|
.from(personalContacts)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(personalContacts.userId, userId),
|
||||||
|
eq(personalContacts.email, updates.email),
|
||||||
|
ne(personalContacts.id, contactId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictError('Kontakt s týmto emailom už existuje')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = { updatedAt: new Date() }
|
||||||
|
|
||||||
|
if (updates.firstName !== undefined) updateData.firstName = updates.firstName
|
||||||
|
if (updates.lastName !== undefined) updateData.lastName = updates.lastName
|
||||||
|
if (updates.phone !== undefined) updateData.phone = updates.phone
|
||||||
|
if (updates.email !== undefined) updateData.email = updates.email
|
||||||
|
if (updates.secondaryEmail !== undefined) updateData.secondaryEmail = updates.secondaryEmail
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(personalContacts)
|
||||||
|
.set(updateData)
|
||||||
|
.where(and(eq(personalContacts.id, contactId), eq(personalContacts.userId, userId)))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new NotFoundError('Kontakt nenájdený')
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deletePersonalContact = async (contactId, userId) => {
|
||||||
|
await getContactById(contactId, userId)
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(personalContacts)
|
||||||
|
.where(and(eq(personalContacts.id, contactId), eq(personalContacts.userId, userId)))
|
||||||
|
|
||||||
|
return { success: true, message: 'Kontakt bol odstránený' }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user