diff --git a/src/app.js b/src/app.js index 6fb897a..5de6a47 100644 --- a/src/app.js +++ b/src/app.js @@ -15,6 +15,7 @@ import { apiRateLimiter } from './middlewares/security/rateLimiter.js'; import authRoutes from './routes/auth.routes.js'; import adminRoutes from './routes/admin.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 emailAccountRoutes from './routes/email-account.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/admin', adminRoutes); app.use('/api/contacts', contactRoutes); +app.use('/api/personal-contacts', personalContactRoutes); app.use('/api/emails', crmEmailRoutes); app.use('/api/email-accounts', emailAccountRoutes); app.use('/api/timesheets', timesheetRoutes); diff --git a/src/controllers/personal-contact.controller.js b/src/controllers/personal-contact.controller.js new file mode 100644 index 0000000..2b70588 --- /dev/null +++ b/src/controllers/personal-contact.controller.js @@ -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) + } +} diff --git a/src/db/schema.js b/src/db/schema.js index 7dd67ce..b0ddac0 100644 --- a/src/db/schema.js +++ b/src/db/schema.js @@ -79,6 +79,21 @@ export const contacts = pgTable('contacts', { 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 // Emaily patria k email accountu a sú zdieľané medzi všetkými používateľmi s prístupom export const emails = pgTable('emails', { diff --git a/src/routes/personal-contact.routes.js b/src/routes/personal-contact.routes.js new file mode 100644 index 0000000..777b500 --- /dev/null +++ b/src/routes/personal-contact.routes.js @@ -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 diff --git a/src/services/personal-contact.service.js b/src/services/personal-contact.service.js new file mode 100644 index 0000000..11413c3 --- /dev/null +++ b/src/services/personal-contact.service.js @@ -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ý' } +}