Add dueDate to reminders, remove reminder from notes

Schema changes:
- Added dueDate field to companyReminders table
- Removed reminderDate and reminderSent from notes table

Backend changes:
- Updated company-reminder.service with dueDate handling
- Added getUpcomingReminders function for dashboard
- Simplified note.service (removed reminder logic)
- Updated validators and routes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-12-01 11:21:54 +01:00
parent 947d1d9b99
commit ffaf916f5e
6 changed files with 82 additions and 113 deletions

View File

@@ -203,12 +203,11 @@ export const addCompanyNote = async (req, res) => {
try { try {
const userId = req.userId; const userId = req.userId;
const { companyId } = req.params; const { companyId } = req.params;
const { content, reminderAt } = req.body; const { content } = req.body;
const note = await noteService.createNote(userId, { const note = await noteService.createNote(userId, {
content, content,
companyId, companyId,
reminderDate: reminderAt, // Map reminderAt to reminderDate
}); });
res.status(201).json({ res.status(201).json({
@@ -229,11 +228,10 @@ export const addCompanyNote = async (req, res) => {
export const updateCompanyNote = async (req, res) => { export const updateCompanyNote = async (req, res) => {
try { try {
const { noteId } = req.params; const { noteId } = req.params;
const { content, reminderAt } = req.body; const { content } = req.body;
const note = await noteService.updateNote(noteId, { const note = await noteService.updateNote(noteId, {
content, content,
reminderDate: reminderAt, // Map reminderAt to reminderDate
}); });
res.status(200).json({ res.status(200).json({
@@ -291,9 +289,9 @@ export const getCompanyReminders = async (req, res) => {
export const createCompanyReminder = async (req, res) => { export const createCompanyReminder = async (req, res) => {
try { try {
const { companyId } = req.params; const { companyId } = req.params;
const { description, isChecked } = req.body; const { description, dueDate, isChecked } = req.body;
const reminder = await companyReminderService.createReminder(companyId, { description, isChecked }); const reminder = await companyReminderService.createReminder(companyId, { description, dueDate, isChecked });
res.status(201).json({ res.status(201).json({
success: true, success: true,
@@ -309,9 +307,9 @@ export const createCompanyReminder = async (req, res) => {
export const updateCompanyReminder = async (req, res) => { export const updateCompanyReminder = async (req, res) => {
try { try {
const { companyId, reminderId } = req.params; const { companyId, reminderId } = req.params;
const { description, isChecked } = req.body; const { description, dueDate, isChecked } = req.body;
const reminder = await companyReminderService.updateReminder(companyId, reminderId, { description, isChecked }); const reminder = await companyReminderService.updateReminder(companyId, reminderId, { description, dueDate, isChecked });
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -365,3 +363,17 @@ export const getReminderCountsByCompany = async (_req, res) => {
res.status(error.statusCode || 500).json(errorResponse); res.status(error.statusCode || 500).json(errorResponse);
} }
}; };
export const getUpcomingReminders = async (_req, res) => {
try {
const reminders = await companyReminderService.getUpcomingReminders();
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);
}
};

View File

@@ -132,11 +132,12 @@ export const projects = pgTable('projects', {
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
}); });
// Company reminders table - jednoduché pripomienky naviazané na firmu // Company reminders table - pripomienky naviazané na firmu s dátumom
export const companyReminders = pgTable('company_remind', { export const companyReminders = pgTable('company_remind', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }).notNull(), companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }).notNull(),
description: text('description').notNull(), description: text('description').notNull(),
dueDate: timestamp('due_date'), // kedy má byť splnená
isChecked: boolean('is_checked').default(false).notNull(), isChecked: boolean('is_checked').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),
@@ -181,7 +182,7 @@ export const todoUsers = pgTable('todo_users', {
todoUserUnique: unique('todo_user_unique').on(table.todoId, table.userId), todoUserUnique: unique('todo_user_unique').on(table.todoId, table.userId),
})); }));
// Notes table - poznámky // Notes table - poznámky (bez reminder funkcionalít)
export const notes = pgTable('notes', { export const notes = pgTable('notes', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
title: text('title'), title: text('title'),
@@ -190,8 +191,6 @@ export const notes = pgTable('notes', {
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }), // alebo projektu projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }), // alebo projektu
todoId: uuid('todo_id').references(() => todos.id, { onDelete: 'cascade' }), // alebo todo todoId: uuid('todo_id').references(() => todos.id, { onDelete: 'cascade' }), // alebo todo
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }), // alebo kontaktu contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }), // alebo kontaktu
reminderDate: timestamp('reminder_date'), // dátum a čas pre reminder
reminderSent: boolean('reminder_sent').default(false).notNull(), // či už bol reminder odoslaný
createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }), createdBy: uuid('created_by').references(() => users.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(),

View File

@@ -13,6 +13,7 @@ router.use(authenticate);
// Reminder summaries (must be before :companyId routes) // Reminder summaries (must be before :companyId routes)
router.get('/reminders/summary', companyController.getReminderSummary); router.get('/reminders/summary', companyController.getReminderSummary);
router.get('/reminders/counts', companyController.getReminderCountsByCompany); router.get('/reminders/counts', companyController.getReminderCountsByCompany);
router.get('/reminders/upcoming', companyController.getUpcomingReminders);
// Company unread email summary // Company unread email summary
router.get('/email-unread', companyController.getCompanyUnreadCounts); router.get('/email-unread', companyController.getCompanyUnreadCounts);
@@ -72,7 +73,6 @@ router.post(
validateParams(z.object({ companyId: z.string().uuid() })), validateParams(z.object({ companyId: z.string().uuid() })),
validateBody(z.object({ validateBody(z.object({
content: z.string().min(1), content: z.string().min(1),
reminderAt: z.string().optional().or(z.literal('')),
})), })),
companyController.addCompanyNote companyController.addCompanyNote
); );
@@ -85,7 +85,6 @@ router.patch(
})), })),
validateBody(z.object({ validateBody(z.object({
content: z.string().min(1).optional(), content: z.string().min(1).optional(),
reminderAt: z.string().optional().or(z.literal('').or(z.null())),
})), })),
companyController.updateCompanyNote companyController.updateCompanyNote
); );

View File

@@ -1,11 +1,11 @@
import { db } from '../config/database.js'; import { db } from '../config/database.js';
import { companies, companyReminders } from '../db/schema.js'; import { companies, companyReminders } from '../db/schema.js';
import { eq, desc, sql } from 'drizzle-orm'; import { eq, desc, sql, and, lte, gte, isNull, or } from 'drizzle-orm';
import { NotFoundError, BadRequestError } from '../utils/errors.js'; import { NotFoundError, BadRequestError } from '../utils/errors.js';
const ensureCompanyExists = async (companyId) => { const ensureCompanyExists = async (companyId) => {
const [company] = await db const [company] = await db
.select({ id: companies.id }) .select({ id: companies.id, name: companies.name })
.from(companies) .from(companies)
.where(eq(companies.id, companyId)) .where(eq(companies.id, companyId))
.limit(1); .limit(1);
@@ -13,6 +13,8 @@ const ensureCompanyExists = async (companyId) => {
if (!company) { if (!company) {
throw new NotFoundError('Firma nenájdená'); throw new NotFoundError('Firma nenájdená');
} }
return company;
}; };
const getReminderById = async (reminderId) => { const getReminderById = async (reminderId) => {
@@ -36,7 +38,7 @@ export const getRemindersByCompanyId = async (companyId) => {
.select() .select()
.from(companyReminders) .from(companyReminders)
.where(eq(companyReminders.companyId, companyId)) .where(eq(companyReminders.companyId, companyId))
.orderBy(desc(companyReminders.createdAt)); .orderBy(companyReminders.dueDate, desc(companyReminders.createdAt));
return reminders; return reminders;
}; };
@@ -54,6 +56,7 @@ export const createReminder = async (companyId, data) => {
.values({ .values({
companyId, companyId,
description, description,
dueDate: data.dueDate ? new Date(data.dueDate) : null,
isChecked: data.isChecked ?? false, isChecked: data.isChecked ?? false,
}) })
.returning(); .returning();
@@ -80,6 +83,7 @@ export const updateReminder = async (companyId, reminderId, data) => {
.update(companyReminders) .update(companyReminders)
.set({ .set({
description: trimmedDescription, description: trimmedDescription,
dueDate: data.dueDate !== undefined ? (data.dueDate ? new Date(data.dueDate) : null) : reminder.dueDate,
isChecked: data.isChecked !== undefined ? data.isChecked : reminder.isChecked, isChecked: data.isChecked !== undefined ? data.isChecked : reminder.isChecked,
updatedAt: new Date(), updatedAt: new Date(),
}) })
@@ -131,3 +135,37 @@ export const getReminderCountsByCompany = async () => {
return rows; return rows;
}; };
/**
* Get upcoming reminders for dashboard
* Returns reminders due within the next 5 days that are not checked
* Includes company name for display
*/
export const getUpcomingReminders = async () => {
const now = new Date();
const fiveDaysFromNow = new Date();
fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5);
const reminders = await db
.select({
id: companyReminders.id,
description: companyReminders.description,
dueDate: companyReminders.dueDate,
isChecked: companyReminders.isChecked,
companyId: companyReminders.companyId,
companyName: companies.name,
createdAt: companyReminders.createdAt,
})
.from(companyReminders)
.leftJoin(companies, eq(companyReminders.companyId, companies.id))
.where(
and(
eq(companyReminders.isChecked, false),
lte(companyReminders.dueDate, fiveDaysFromNow),
gte(companyReminders.dueDate, now)
)
)
.orderBy(companyReminders.dueDate);
return reminders;
};

View File

@@ -1,21 +1,8 @@
import { db } from '../config/database.js'; import { db } from '../config/database.js';
import { notes, companies, projects, todos, contacts } from '../db/schema.js'; import { notes, companies, projects, todos, contacts } from '../db/schema.js';
import { eq, desc, ilike, or, and, lte, isNull, not } from 'drizzle-orm'; import { eq, desc, ilike, or, and } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js'; import { NotFoundError } from '../utils/errors.js';
/**
* Map note fields for frontend compatibility
* reminderDate → reminderAt
*/
const mapNoteForFrontend = (note) => {
if (!note) return note;
const { reminderDate, ...rest } = note;
return {
...rest,
reminderAt: reminderDate,
};
};
/** /**
* Get all notes * Get all notes
* Optionally filter by search, company, project, todo, or contact * Optionally filter by search, company, project, todo, or contact
@@ -56,8 +43,7 @@ export const getAllNotes = async (filters = {}) => {
query = query.where(and(...conditions)); query = query.where(and(...conditions));
} }
const result = await query.orderBy(desc(notes.createdAt)); return await query.orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
}; };
/** /**
@@ -74,14 +60,14 @@ export const getNoteById = async (noteId) => {
throw new NotFoundError('Poznámka nenájdená'); throw new NotFoundError('Poznámka nenájdená');
} }
return mapNoteForFrontend(note); return note;
}; };
/** /**
* Create new note * Create new note
*/ */
export const createNote = async (userId, data) => { export const createNote = async (userId, data) => {
const { title, content, companyId, projectId, todoId, contactId, reminderDate } = data; const { title, content, companyId, projectId, todoId, contactId } = data;
// Verify company exists if provided // Verify company exists if provided
if (companyId) { if (companyId) {
@@ -144,13 +130,11 @@ export const createNote = async (userId, data) => {
projectId: projectId || null, projectId: projectId || null,
todoId: todoId || null, todoId: todoId || null,
contactId: contactId || null, contactId: contactId || null,
reminderDate: reminderDate ? new Date(reminderDate) : null,
reminderSent: false,
createdBy: userId, createdBy: userId,
}) })
.returning(); .returning();
return mapNoteForFrontend(newNote); return newNote;
}; };
/** /**
@@ -159,7 +143,7 @@ export const createNote = async (userId, data) => {
export const updateNote = async (noteId, data) => { export const updateNote = async (noteId, data) => {
const note = await getNoteById(noteId); const note = await getNoteById(noteId);
const { title, content, companyId, projectId, todoId, contactId, reminderDate } = data; const { title, content, companyId, projectId, todoId, contactId } = data;
// Verify company exists if being changed // Verify company exists if being changed
if (companyId !== undefined && companyId !== null && companyId !== note.companyId) { if (companyId !== undefined && companyId !== null && companyId !== note.companyId) {
@@ -222,14 +206,12 @@ export const updateNote = async (noteId, data) => {
projectId: projectId !== undefined ? projectId : note.projectId, projectId: projectId !== undefined ? projectId : note.projectId,
todoId: todoId !== undefined ? todoId : note.todoId, todoId: todoId !== undefined ? todoId : note.todoId,
contactId: contactId !== undefined ? contactId : note.contactId, contactId: contactId !== undefined ? contactId : note.contactId,
reminderDate: reminderDate !== undefined ? (reminderDate ? new Date(reminderDate) : null) : note.reminderDate,
reminderSent: reminderDate !== undefined ? false : note.reminderSent, // Reset reminderSent if reminderDate changes
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(notes.id, noteId)) .where(eq(notes.id, noteId))
.returning(); .returning();
return mapNoteForFrontend(updated); return updated;
}; };
/** /**
@@ -247,103 +229,42 @@ export const deleteNote = async (noteId) => {
* Get notes by company ID * Get notes by company ID
*/ */
export const getNotesByCompanyId = async (companyId) => { export const getNotesByCompanyId = async (companyId) => {
const result = await db return await db
.select() .select()
.from(notes) .from(notes)
.where(eq(notes.companyId, companyId)) .where(eq(notes.companyId, companyId))
.orderBy(desc(notes.createdAt)); .orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
}; };
/** /**
* Get notes by project ID * Get notes by project ID
*/ */
export const getNotesByProjectId = async (projectId) => { export const getNotesByProjectId = async (projectId) => {
const result = await db return await db
.select() .select()
.from(notes) .from(notes)
.where(eq(notes.projectId, projectId)) .where(eq(notes.projectId, projectId))
.orderBy(desc(notes.createdAt)); .orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
}; };
/** /**
* Get notes by todo ID * Get notes by todo ID
*/ */
export const getNotesByTodoId = async (todoId) => { export const getNotesByTodoId = async (todoId) => {
const result = await db return await db
.select() .select()
.from(notes) .from(notes)
.where(eq(notes.todoId, todoId)) .where(eq(notes.todoId, todoId))
.orderBy(desc(notes.createdAt)); .orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
}; };
/** /**
* Get notes by contact ID * Get notes by contact ID
*/ */
export const getNotesByContactId = async (contactId) => { export const getNotesByContactId = async (contactId) => {
const result = await db return await db
.select() .select()
.from(notes) .from(notes)
.where(eq(notes.contactId, contactId)) .where(eq(notes.contactId, contactId))
.orderBy(desc(notes.createdAt)); .orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
};
/**
* Get pending reminders (reminders that are due and not sent)
*/
export const getPendingReminders = async () => {
const now = new Date();
const result = await db
.select()
.from(notes)
.where(
and(
not(isNull(notes.reminderDate)),
lte(notes.reminderDate, now),
eq(notes.reminderSent, false)
)
)
.orderBy(notes.reminderDate);
return result.map(mapNoteForFrontend);
};
/**
* Mark reminder as sent
*/
export const markReminderAsSent = async (noteId) => {
const [updated] = await db
.update(notes)
.set({
reminderSent: true,
updatedAt: new Date(),
})
.where(eq(notes.id, noteId))
.returning();
return mapNoteForFrontend(updated);
};
/**
* Get upcoming reminders for a user (created by user, not sent yet)
*/
export const getUpcomingRemindersForUser = async (userId) => {
const now = new Date();
const result = await db
.select()
.from(notes)
.where(
and(
eq(notes.createdBy, userId),
not(isNull(notes.reminderDate)),
lte(notes.reminderDate, now),
eq(notes.reminderSent, false)
)
)
.orderBy(notes.reminderDate);
return result.map(mapNoteForFrontend);
}; };

View File

@@ -81,7 +81,7 @@ export const updateTodoSchema = z.object({
dueDate: z.string().optional().or(z.literal('').or(z.null())), dueDate: z.string().optional().or(z.literal('').or(z.null())),
}); });
// Note validators // Note validators (without reminder functionality)
export const createNoteSchema = z.object({ export const createNoteSchema = z.object({
title: z.string().max(255).optional(), title: z.string().max(255).optional(),
content: z content: z
@@ -94,7 +94,6 @@ export const createNoteSchema = z.object({
projectId: z.string().uuid('Neplatný formát project ID').optional().or(z.literal('')), projectId: z.string().uuid('Neplatný formát project ID').optional().or(z.literal('')),
todoId: z.string().uuid('Neplatný formát todo ID').optional().or(z.literal('')), todoId: z.string().uuid('Neplatný formát todo ID').optional().or(z.literal('')),
contactId: z.string().uuid('Neplatný formát contact ID').optional().or(z.literal('')), contactId: z.string().uuid('Neplatný formát contact ID').optional().or(z.literal('')),
reminderDate: z.string().optional().or(z.literal('')),
}); });
export const updateNoteSchema = z.object({ export const updateNoteSchema = z.object({
@@ -104,21 +103,22 @@ export const updateNoteSchema = z.object({
projectId: z.string().uuid('Neplatný formát project ID').optional().or(z.literal('').or(z.null())), projectId: z.string().uuid('Neplatný formát project ID').optional().or(z.literal('').or(z.null())),
todoId: z.string().uuid('Neplatný formát todo ID').optional().or(z.literal('').or(z.null())), todoId: z.string().uuid('Neplatný formát todo ID').optional().or(z.literal('').or(z.null())),
contactId: z.string().uuid('Neplatný formát contact ID').optional().or(z.literal('').or(z.null())), contactId: z.string().uuid('Neplatný formát contact ID').optional().or(z.literal('').or(z.null())),
reminderDate: z.string().optional().or(z.literal('').or(z.null())),
}); });
// Company reminder validators // Company reminder validators (with dueDate)
export const createCompanyReminderSchema = z.object({ export const createCompanyReminderSchema = z.object({
description: z.string().min(1).max(1000), description: z.string().min(1).max(1000),
dueDate: z.string().optional().or(z.literal('')),
isChecked: z.boolean().optional(), isChecked: z.boolean().optional(),
}); });
export const updateCompanyReminderSchema = z.object({ export const updateCompanyReminderSchema = z.object({
description: z.string().min(1).max(1000).optional(), description: z.string().min(1).max(1000).optional(),
dueDate: z.string().optional().or(z.literal('').or(z.null())),
isChecked: z.boolean().optional(), isChecked: z.boolean().optional(),
}).refine( }).refine(
(data) => data.description !== undefined || data.isChecked !== undefined, (data) => data.description !== undefined || data.isChecked !== undefined || data.dueDate !== undefined,
{ message: 'Je potrebné zadať description alebo isChecked' } { message: 'Je potrebné zadať description, dueDate alebo isChecked' }
); );
// Time Tracking validators // Time Tracking validators