Add company reminders feature

This commit is contained in:
richardtekula
2025-11-25 10:28:18 +01:00
parent 043eeccb77
commit 440585852d
7 changed files with 254 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
import * as companyService from '../services/company.service.js'; import * as companyService from '../services/company.service.js';
import * as noteService from '../services/note.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'; import { formatErrorResponse } from '../utils/errors.js';
/** /**
@@ -221,3 +222,76 @@ export const deleteCompanyNote = async (req, res) => {
res.status(error.statusCode || 500).json(errorResponse); 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);
}
};

View File

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

View File

@@ -116,6 +116,16 @@ export const companies = pgTable('companies', {
updatedAt: timestamp('updated_at').defaultNow().notNull(), 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(),
});
// Projects table - projekty // Projects table - projekty
export const projects = pgTable('projects', { export const projects = pgTable('projects', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),

View File

@@ -2,7 +2,7 @@ import express from 'express';
import * as companyController from '../controllers/company.controller.js'; import * as companyController from '../controllers/company.controller.js';
import { authenticate } from '../middlewares/auth/authMiddleware.js'; import { authenticate } from '../middlewares/auth/authMiddleware.js';
import { validateBody, validateParams } from '../middlewares/security/validateInput.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'; import { z } from 'zod';
const router = express.Router(); const router = express.Router();
@@ -85,4 +85,37 @@ router.delete(
companyController.deleteCompanyNote 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; export default router;

View File

@@ -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ý' };
};

View File

@@ -1,5 +1,5 @@
import { db } from '../config/database.js'; 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 { eq, desc, ilike, or, and } from 'drizzle-orm';
import { NotFoundError, ConflictError } from '../utils/errors.js'; import { NotFoundError, ConflictError } from '../utils/errors.js';
@@ -154,10 +154,17 @@ export const getCompanyWithRelations = async (companyId) => {
.where(eq(notes.companyId, companyId)) .where(eq(notes.companyId, companyId))
.orderBy(desc(notes.createdAt)); .orderBy(desc(notes.createdAt));
const companyReminderList = await db
.select()
.from(companyReminders)
.where(eq(companyReminders.companyId, companyId))
.orderBy(desc(companyReminders.createdAt));
return { return {
...company, ...company,
projects: companyProjects, projects: companyProjects,
todos: companyTodos, todos: companyTodos,
notes: companyNotes, notes: companyNotes,
reminders: companyReminderList,
}; };
}; };

View File

@@ -106,6 +106,20 @@ export const updateNoteSchema = z.object({
reminderDate: z.string().optional().or(z.literal('').or(z.null())), 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 // Time Tracking validators
const optionalUuid = (message) => const optionalUuid = (message) =>
z z