diff --git a/src/controllers/company.controller.js b/src/controllers/company.controller.js index b10b138..47323f2 100644 --- a/src/controllers/company.controller.js +++ b/src/controllers/company.controller.js @@ -1,5 +1,6 @@ import * as companyService from '../services/company.service.js'; import * as noteService from '../services/note.service.js'; +import * as companyReminderService from '../services/company-reminder.service.js'; import { formatErrorResponse } from '../utils/errors.js'; /** @@ -221,3 +222,76 @@ export const deleteCompanyNote = async (req, res) => { res.status(error.statusCode || 500).json(errorResponse); } }; + +/** + * Company reminders + * CRUD for /api/companies/:companyId/reminders + */ +export const getCompanyReminders = async (req, res) => { + try { + const { companyId } = req.params; + + const reminders = await companyReminderService.getRemindersByCompanyId(companyId); + + res.status(200).json({ + success: true, + count: reminders.length, + data: reminders, + }); + } catch (error) { + const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development'); + res.status(error.statusCode || 500).json(errorResponse); + } +}; + +export const createCompanyReminder = async (req, res) => { + try { + const { companyId } = req.params; + const { description, isChecked } = req.body; + + const reminder = await companyReminderService.createReminder(companyId, { description, isChecked }); + + res.status(201).json({ + success: true, + data: reminder, + message: 'Reminder bol pridaný', + }); + } catch (error) { + const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development'); + res.status(error.statusCode || 500).json(errorResponse); + } +}; + +export const updateCompanyReminder = async (req, res) => { + try { + const { companyId, reminderId } = req.params; + const { description, isChecked } = req.body; + + const reminder = await companyReminderService.updateReminder(companyId, reminderId, { description, isChecked }); + + res.status(200).json({ + success: true, + data: reminder, + message: 'Reminder bol aktualizovaný', + }); + } catch (error) { + const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development'); + res.status(error.statusCode || 500).json(errorResponse); + } +}; + +export const deleteCompanyReminder = async (req, res) => { + try { + const { companyId, reminderId } = req.params; + + const result = await companyReminderService.deleteReminder(companyId, reminderId); + + res.status(200).json({ + success: true, + message: result.message, + }); + } catch (error) { + const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development'); + res.status(error.statusCode || 500).json(errorResponse); + } +}; diff --git a/src/db/migrations/0007_add_company_remind_table.sql b/src/db/migrations/0007_add_company_remind_table.sql new file mode 100644 index 0000000..fba40fb --- /dev/null +++ b/src/db/migrations/0007_add_company_remind_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS "company_remind" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "description" text NOT NULL, + "is_checked" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "company_remind" ADD CONSTRAINT "company_remind_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "company_remind_company_id_idx" ON "company_remind" ("company_id"); diff --git a/src/db/schema.js b/src/db/schema.js index e04845a..b6ca7c5 100644 --- a/src/db/schema.js +++ b/src/db/schema.js @@ -111,6 +111,7 @@ export const companies = pgTable('companies', { phone: text('phone'), email: text('email'), website: text('website'), + isActive: boolean('is_active').default(true).notNull(), // či je firma aktívna createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), @@ -130,6 +131,16 @@ export const projects = pgTable('projects', { updatedAt: timestamp('updated_at').defaultNow().notNull(), }); +// Company reminders table - jednoduché pripomienky naviazané na firmu +export const companyReminders = pgTable('company_remind', { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }).notNull(), + description: text('description').notNull(), + isChecked: boolean('is_checked').default(false).notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + // Project Users - many-to-many medzi projects a users (tím projektu) export const projectUsers = pgTable('project_users', { id: uuid('id').primaryKey().defaultRandom(), diff --git a/src/routes/company.routes.js b/src/routes/company.routes.js index ff44ce8..120c075 100644 --- a/src/routes/company.routes.js +++ b/src/routes/company.routes.js @@ -2,7 +2,7 @@ import express from 'express'; import * as companyController from '../controllers/company.controller.js'; import { authenticate } from '../middlewares/auth/authMiddleware.js'; import { validateBody, validateParams } from '../middlewares/security/validateInput.js'; -import { createCompanySchema, updateCompanySchema } from '../validators/crm.validators.js'; +import { createCompanySchema, updateCompanySchema, createCompanyReminderSchema, updateCompanyReminderSchema } from '../validators/crm.validators.js'; import { z } from 'zod'; const router = express.Router(); @@ -85,4 +85,37 @@ router.delete( companyController.deleteCompanyNote ); +// Company reminders +router.get( + '/:companyId/reminders', + validateParams(z.object({ companyId: z.string().uuid() })), + companyController.getCompanyReminders +); + +router.post( + '/:companyId/reminders', + validateParams(z.object({ companyId: z.string().uuid() })), + validateBody(createCompanyReminderSchema), + companyController.createCompanyReminder +); + +router.patch( + '/:companyId/reminders/:reminderId', + validateParams(z.object({ + companyId: z.string().uuid(), + reminderId: z.string().uuid() + })), + validateBody(updateCompanyReminderSchema), + companyController.updateCompanyReminder +); + +router.delete( + '/:companyId/reminders/:reminderId', + validateParams(z.object({ + companyId: z.string().uuid(), + reminderId: z.string().uuid() + })), + companyController.deleteCompanyReminder +); + export default router; diff --git a/src/services/company-reminder.service.js b/src/services/company-reminder.service.js new file mode 100644 index 0000000..68d3913 --- /dev/null +++ b/src/services/company-reminder.service.js @@ -0,0 +1,102 @@ +import { db } from '../config/database.js'; +import { companies, companyReminders } from '../db/schema.js'; +import { eq, desc } from 'drizzle-orm'; +import { NotFoundError, BadRequestError } from '../utils/errors.js'; + +const ensureCompanyExists = async (companyId) => { + const [company] = await db + .select({ id: companies.id }) + .from(companies) + .where(eq(companies.id, companyId)) + .limit(1); + + if (!company) { + throw new NotFoundError('Firma nenájdená'); + } +}; + +const getReminderById = async (reminderId) => { + const [reminder] = await db + .select() + .from(companyReminders) + .where(eq(companyReminders.id, reminderId)) + .limit(1); + + if (!reminder) { + throw new NotFoundError('Reminder nenájdený'); + } + + return reminder; +}; + +export const getRemindersByCompanyId = async (companyId) => { + await ensureCompanyExists(companyId); + + const reminders = await db + .select() + .from(companyReminders) + .where(eq(companyReminders.companyId, companyId)) + .orderBy(desc(companyReminders.createdAt)); + + return reminders; +}; + +export const createReminder = async (companyId, data) => { + await ensureCompanyExists(companyId); + + const description = data.description?.trim(); + if (!description) { + throw new BadRequestError('Popis pripomienky je povinný'); + } + + const [reminder] = await db + .insert(companyReminders) + .values({ + companyId, + description, + isChecked: data.isChecked ?? false, + }) + .returning(); + + return reminder; +}; + +export const updateReminder = async (companyId, reminderId, data) => { + const reminder = await getReminderById(reminderId); + + if (reminder.companyId !== companyId) { + throw new NotFoundError('Reminder nenájdený'); + } + + const trimmedDescription = data.description !== undefined + ? data.description.trim() + : reminder.description; + + if (data.description !== undefined && !trimmedDescription) { + throw new BadRequestError('Popis pripomienky je povinný'); + } + + const [updatedReminder] = await db + .update(companyReminders) + .set({ + description: trimmedDescription, + isChecked: data.isChecked !== undefined ? data.isChecked : reminder.isChecked, + updatedAt: new Date(), + }) + .where(eq(companyReminders.id, reminderId)) + .returning(); + + return updatedReminder; +}; + +export const deleteReminder = async (companyId, reminderId) => { + const reminder = await getReminderById(reminderId); + + if (reminder.companyId !== companyId) { + throw new NotFoundError('Reminder nenájdený'); + } + + await db.delete(companyReminders).where(eq(companyReminders.id, reminderId)); + + return { success: true, message: 'Reminder bol odstránený' }; +}; diff --git a/src/services/company.service.js b/src/services/company.service.js index a35b4cb..1f067d5 100644 --- a/src/services/company.service.js +++ b/src/services/company.service.js @@ -1,5 +1,5 @@ import { db } from '../config/database.js'; -import { companies, projects, todos, notes } from '../db/schema.js'; +import { companies, projects, todos, notes, companyReminders } from '../db/schema.js'; import { eq, desc, ilike, or, and } from 'drizzle-orm'; import { NotFoundError, ConflictError } from '../utils/errors.js'; @@ -82,7 +82,7 @@ export const createCompany = async (userId, data) => { export const updateCompany = async (companyId, data) => { const company = await getCompanyById(companyId); - const { name, description, address, city, country, phone, email, website } = data; + const { name, description, address, city, country, phone, email, website, isActive } = data; // If name is being changed, check for duplicates if (name && name !== company.name) { @@ -108,6 +108,7 @@ export const updateCompany = async (companyId, data) => { phone: phone !== undefined ? phone : company.phone, email: email !== undefined ? email : company.email, website: website !== undefined ? website : company.website, + isActive: isActive !== undefined ? isActive : company.isActive, updatedAt: new Date(), }) .where(eq(companies.id, companyId)) @@ -154,10 +155,17 @@ export const getCompanyWithRelations = async (companyId) => { .where(eq(notes.companyId, companyId)) .orderBy(desc(notes.createdAt)); + const companyReminderList = await db + .select() + .from(companyReminders) + .where(eq(companyReminders.companyId, companyId)) + .orderBy(desc(companyReminders.createdAt)); + return { ...company, projects: companyProjects, todos: companyTodos, notes: companyNotes, + reminders: companyReminderList, }; }; diff --git a/src/validators/crm.validators.js b/src/validators/crm.validators.js index f972781..9271c28 100644 --- a/src/validators/crm.validators.js +++ b/src/validators/crm.validators.js @@ -26,6 +26,7 @@ export const updateCompanySchema = z.object({ phone: z.string().max(50).optional(), email: z.string().email('Neplatný formát emailu').max(255).optional().or(z.literal('')), website: z.string().url('Neplatný formát URL').max(255).optional().or(z.literal('')), + isActive: z.boolean().optional(), }); // Project validators @@ -106,6 +107,20 @@ export const updateNoteSchema = z.object({ reminderDate: z.string().optional().or(z.literal('').or(z.null())), }); +// Company reminder validators +export const createCompanyReminderSchema = z.object({ + description: z.string().min(1).max(1000), + isChecked: z.boolean().optional(), +}); + +export const updateCompanyReminderSchema = z.object({ + description: z.string().min(1).max(1000).optional(), + isChecked: z.boolean().optional(), +}).refine( + (data) => data.description !== undefined || data.isChecked !== undefined, + { message: 'Je potrebné zadať description alebo isChecked' } +); + // Time Tracking validators const optionalUuid = (message) => z