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:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user