From ffaf916f5e825df4f079586de513784096181a0c Mon Sep 17 00:00:00 2001 From: richardtekula Date: Mon, 1 Dec 2025 11:21:54 +0100 Subject: [PATCH] Add dueDate to reminders, remove reminder from notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/controllers/company.controller.js | 28 +++++-- src/db/schema.js | 7 +- src/routes/company.routes.js | 3 +- src/services/company-reminder.service.js | 44 +++++++++- src/services/note.service.js | 101 +++-------------------- src/validators/crm.validators.js | 12 +-- 6 files changed, 82 insertions(+), 113 deletions(-) diff --git a/src/controllers/company.controller.js b/src/controllers/company.controller.js index 71191f8..857f6ee 100644 --- a/src/controllers/company.controller.js +++ b/src/controllers/company.controller.js @@ -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); + } +}; diff --git a/src/db/schema.js b/src/db/schema.js index 6e91bba..2bc64cf 100644 --- a/src/db/schema.js +++ b/src/db/schema.js @@ -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(), diff --git a/src/routes/company.routes.js b/src/routes/company.routes.js index 9ba58d7..3a57a66 100644 --- a/src/routes/company.routes.js +++ b/src/routes/company.routes.js @@ -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 ); diff --git a/src/services/company-reminder.service.js b/src/services/company-reminder.service.js index ecc7860..9b1453c 100644 --- a/src/services/company-reminder.service.js +++ b/src/services/company-reminder.service.js @@ -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; +}; diff --git a/src/services/note.service.js b/src/services/note.service.js index 4bfa093..0ca5bad 100644 --- a/src/services/note.service.js +++ b/src/services/note.service.js @@ -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); }; diff --git a/src/validators/crm.validators.js b/src/validators/crm.validators.js index 9271c28..1a78371 100644 --- a/src/validators/crm.validators.js +++ b/src/validators/crm.validators.js @@ -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