add contacts to crm & display on dashboard

This commit is contained in:
richardtekula
2025-12-08 10:36:43 +01:00
parent 38e2c5970a
commit 918af3a843
5 changed files with 220 additions and 0 deletions

View File

@@ -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);

View 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)
}
}

View File

@@ -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', {

View 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

View 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ý' }
}