Add company reminders feature
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
12
src/db/migrations/0007_add_company_remind_table.sql
Normal file
12
src/db/migrations/0007_add_company_remind_table.sql
Normal 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");
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
102
src/services/company-reminder.service.js
Normal file
102
src/services/company-reminder.service.js
Normal 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ý' };
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user