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 {
const userId = req.userId;
const { companyId } = req.params;
const { content, reminderAt } = req.body;
const { content } = req.body;
const note = await noteService.createNote(userId, {
content,
companyId,
reminderDate: reminderAt, // Map reminderAt to reminderDate
});
res.status(201).json({
@@ -229,11 +228,10 @@ export const addCompanyNote = async (req, res) => {
export const updateCompanyNote = async (req, res) => {
try {
const { noteId } = req.params;
const { content, reminderAt } = req.body;
const { content } = req.body;
const note = await noteService.updateNote(noteId, {
content,
reminderDate: reminderAt, // Map reminderAt to reminderDate
});
res.status(200).json({
@@ -291,9 +289,9 @@ export const getCompanyReminders = async (req, res) => {
export const createCompanyReminder = async (req, res) => {
try {
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({
success: true,
@@ -309,9 +307,9 @@ export const createCompanyReminder = async (req, res) => {
export const updateCompanyReminder = async (req, res) => {
try {
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({
success: true,
@@ -365,3 +363,17 @@ export const getReminderCountsByCompany = async (_req, res) => {
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(),
});
// Company reminders table - jednoduché pripomienky naviazané na firmu
// Company reminders table - pripomienky naviazané na firmu s dátumom
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(),
dueDate: timestamp('due_date'), // kedy má byť splnená
isChecked: boolean('is_checked').default(false).notNull(),
createdAt: timestamp('created_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),
}));
// Notes table - poznámky
// Notes table - poznámky (bez reminder funkcionalít)
export const notes = pgTable('notes', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title'),
@@ -190,8 +191,6 @@ export const notes = pgTable('notes', {
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'cascade' }), // alebo projektu
todoId: uuid('todo_id').references(() => todos.id, { onDelete: 'cascade' }), // alebo todo
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' }),
createdAt: timestamp('created_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)
router.get('/reminders/summary', companyController.getReminderSummary);
router.get('/reminders/counts', companyController.getReminderCountsByCompany);
router.get('/reminders/upcoming', companyController.getUpcomingReminders);
// Company unread email summary
router.get('/email-unread', companyController.getCompanyUnreadCounts);
@@ -72,7 +73,6 @@ router.post(
validateParams(z.object({ companyId: z.string().uuid() })),
validateBody(z.object({
content: z.string().min(1),
reminderAt: z.string().optional().or(z.literal('')),
})),
companyController.addCompanyNote
);
@@ -85,7 +85,6 @@ router.patch(
})),
validateBody(z.object({
content: z.string().min(1).optional(),
reminderAt: z.string().optional().or(z.literal('').or(z.null())),
})),
companyController.updateCompanyNote
);

View File

@@ -1,11 +1,11 @@
import { db } from '../config/database.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';
const ensureCompanyExists = async (companyId) => {
const [company] = await db
.select({ id: companies.id })
.select({ id: companies.id, name: companies.name })
.from(companies)
.where(eq(companies.id, companyId))
.limit(1);
@@ -13,6 +13,8 @@ const ensureCompanyExists = async (companyId) => {
if (!company) {
throw new NotFoundError('Firma nenájdená');
}
return company;
};
const getReminderById = async (reminderId) => {
@@ -36,7 +38,7 @@ export const getRemindersByCompanyId = async (companyId) => {
.select()
.from(companyReminders)
.where(eq(companyReminders.companyId, companyId))
.orderBy(desc(companyReminders.createdAt));
.orderBy(companyReminders.dueDate, desc(companyReminders.createdAt));
return reminders;
};
@@ -54,6 +56,7 @@ export const createReminder = async (companyId, data) => {
.values({
companyId,
description,
dueDate: data.dueDate ? new Date(data.dueDate) : null,
isChecked: data.isChecked ?? false,
})
.returning();
@@ -80,6 +83,7 @@ export const updateReminder = async (companyId, reminderId, data) => {
.update(companyReminders)
.set({
description: trimmedDescription,
dueDate: data.dueDate !== undefined ? (data.dueDate ? new Date(data.dueDate) : null) : reminder.dueDate,
isChecked: data.isChecked !== undefined ? data.isChecked : reminder.isChecked,
updatedAt: new Date(),
})
@@ -131,3 +135,37 @@ export const getReminderCountsByCompany = async () => {
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 { 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';
/**
* 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
* Optionally filter by search, company, project, todo, or contact
@@ -56,8 +43,7 @@ export const getAllNotes = async (filters = {}) => {
query = query.where(and(...conditions));
}
const result = await query.orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
return await query.orderBy(desc(notes.createdAt));
};
/**
@@ -74,14 +60,14 @@ export const getNoteById = async (noteId) => {
throw new NotFoundError('Poznámka nenájdená');
}
return mapNoteForFrontend(note);
return note;
};
/**
* Create new note
*/
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
if (companyId) {
@@ -144,13 +130,11 @@ export const createNote = async (userId, data) => {
projectId: projectId || null,
todoId: todoId || null,
contactId: contactId || null,
reminderDate: reminderDate ? new Date(reminderDate) : null,
reminderSent: false,
createdBy: userId,
})
.returning();
return mapNoteForFrontend(newNote);
return newNote;
};
/**
@@ -159,7 +143,7 @@ export const createNote = async (userId, data) => {
export const updateNote = async (noteId, data) => {
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
if (companyId !== undefined && companyId !== null && companyId !== note.companyId) {
@@ -222,14 +206,12 @@ export const updateNote = async (noteId, data) => {
projectId: projectId !== undefined ? projectId : note.projectId,
todoId: todoId !== undefined ? todoId : note.todoId,
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(),
})
.where(eq(notes.id, noteId))
.returning();
return mapNoteForFrontend(updated);
return updated;
};
/**
@@ -247,103 +229,42 @@ export const deleteNote = async (noteId) => {
* Get notes by company ID
*/
export const getNotesByCompanyId = async (companyId) => {
const result = await db
return await db
.select()
.from(notes)
.where(eq(notes.companyId, companyId))
.orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
};
/**
* Get notes by project ID
*/
export const getNotesByProjectId = async (projectId) => {
const result = await db
return await db
.select()
.from(notes)
.where(eq(notes.projectId, projectId))
.orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
};
/**
* Get notes by todo ID
*/
export const getNotesByTodoId = async (todoId) => {
const result = await db
return await db
.select()
.from(notes)
.where(eq(notes.todoId, todoId))
.orderBy(desc(notes.createdAt));
return result.map(mapNoteForFrontend);
};
/**
* Get notes by contact ID
*/
export const getNotesByContactId = async (contactId) => {
const result = await db
return await db
.select()
.from(notes)
.where(eq(notes.contactId, contactId))
.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())),
});
// Note validators
// Note validators (without reminder functionality)
export const createNoteSchema = z.object({
title: z.string().max(255).optional(),
content: z
@@ -94,7 +94,6 @@ export const createNoteSchema = z.object({
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('')),
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({
@@ -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())),
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())),
reminderDate: z.string().optional().or(z.literal('').or(z.null())),
});
// Company reminder validators
// Company reminder validators (with dueDate)
export const createCompanyReminderSchema = z.object({
description: z.string().min(1).max(1000),
dueDate: z.string().optional().or(z.literal('')),
isChecked: z.boolean().optional(),
});
export const updateCompanyReminderSchema = z.object({
description: z.string().min(1).max(1000).optional(),
dueDate: z.string().optional().or(z.literal('').or(z.null())),
isChecked: z.boolean().optional(),
}).refine(
(data) => data.description !== undefined || data.isChecked !== undefined,
{ message: 'Je potrebné zadať description alebo isChecked' }
(data) => data.description !== undefined || data.isChecked !== undefined || data.dueDate !== undefined,
{ message: 'Je potrebné zadať description, dueDate alebo isChecked' }
);
// Time Tracking validators