fix email issues, add company,project,todos
This commit is contained in:
163
src/services/company.service.js
Normal file
163
src/services/company.service.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import { db } from '../config/database.js';
|
||||
import { companies, projects, todos, notes } from '../db/schema.js';
|
||||
import { eq, desc, ilike, or, and } from 'drizzle-orm';
|
||||
import { NotFoundError, ConflictError } from '../utils/errors.js';
|
||||
|
||||
/**
|
||||
* Get all companies
|
||||
* Optionally filter by search term
|
||||
*/
|
||||
export const getAllCompanies = async (searchTerm = null) => {
|
||||
let query = db.select().from(companies);
|
||||
|
||||
if (searchTerm) {
|
||||
query = query.where(
|
||||
or(
|
||||
ilike(companies.name, `%${searchTerm}%`),
|
||||
ilike(companies.email, `%${searchTerm}%`),
|
||||
ilike(companies.city, `%${searchTerm}%`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const result = await query.orderBy(desc(companies.createdAt));
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get company by ID
|
||||
*/
|
||||
export const getCompanyById = async (companyId) => {
|
||||
const [company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, companyId))
|
||||
.limit(1);
|
||||
|
||||
if (!company) {
|
||||
throw new NotFoundError('Firma nenájdená');
|
||||
}
|
||||
|
||||
return company;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new company
|
||||
*/
|
||||
export const createCompany = async (userId, data) => {
|
||||
const { name, description, address, city, country, phone, email, website } = data;
|
||||
|
||||
// Check if company with same name already exists
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.name, name))
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictError('Firma s týmto názvom už existuje');
|
||||
}
|
||||
|
||||
const [newCompany] = await db
|
||||
.insert(companies)
|
||||
.values({
|
||||
name,
|
||||
description: description || null,
|
||||
address: address || null,
|
||||
city: city || null,
|
||||
country: country || null,
|
||||
phone: phone || null,
|
||||
email: email || null,
|
||||
website: website || null,
|
||||
createdBy: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newCompany;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update company
|
||||
*/
|
||||
export const updateCompany = async (companyId, data) => {
|
||||
const company = await getCompanyById(companyId);
|
||||
|
||||
const { name, description, address, city, country, phone, email, website } = data;
|
||||
|
||||
// If name is being changed, check for duplicates
|
||||
if (name && name !== company.name) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(and(eq(companies.name, name), eq(companies.id, companyId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing && existing.id !== companyId) {
|
||||
throw new ConflictError('Firma s týmto názvom už existuje');
|
||||
}
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(companies)
|
||||
.set({
|
||||
name: name !== undefined ? name : company.name,
|
||||
description: description !== undefined ? description : company.description,
|
||||
address: address !== undefined ? address : company.address,
|
||||
city: city !== undefined ? city : company.city,
|
||||
country: country !== undefined ? country : company.country,
|
||||
phone: phone !== undefined ? phone : company.phone,
|
||||
email: email !== undefined ? email : company.email,
|
||||
website: website !== undefined ? website : company.website,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(companies.id, companyId))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete company
|
||||
*/
|
||||
export const deleteCompany = async (companyId) => {
|
||||
await getCompanyById(companyId); // Check if exists
|
||||
|
||||
await db.delete(companies).where(eq(companies.id, companyId));
|
||||
|
||||
return { success: true, message: 'Firma bola odstránená' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get company with related data (projects, todos, notes)
|
||||
*/
|
||||
export const getCompanyWithRelations = async (companyId) => {
|
||||
const company = await getCompanyById(companyId);
|
||||
|
||||
// Get related projects
|
||||
const companyProjects = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.companyId, companyId))
|
||||
.orderBy(desc(projects.createdAt));
|
||||
|
||||
// Get related todos
|
||||
const companyTodos = await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.companyId, companyId))
|
||||
.orderBy(desc(todos.createdAt));
|
||||
|
||||
// Get related notes
|
||||
const companyNotes = await db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(eq(notes.companyId, companyId))
|
||||
.orderBy(desc(notes.createdAt));
|
||||
|
||||
return {
|
||||
...company,
|
||||
projects: companyProjects,
|
||||
todos: companyTodos,
|
||||
notes: companyNotes,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '../config/database.js';
|
||||
import { contacts, emails } from '../db/schema.js';
|
||||
import { contacts, emails, companies } from '../db/schema.js';
|
||||
import { eq, and, desc, or, ne } from 'drizzle-orm';
|
||||
import { NotFoundError, ConflictError } from '../utils/errors.js';
|
||||
import { syncEmailsFromSender } from './jmap.service.js';
|
||||
@@ -156,3 +156,91 @@ export const updateContact = async (contactId, emailAccountId, { name, notes })
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Link company to contact
|
||||
*/
|
||||
export const linkCompanyToContact = async (contactId, emailAccountId, companyId) => {
|
||||
const contact = await getContactById(contactId, emailAccountId);
|
||||
|
||||
const [updated] = await db
|
||||
.update(contacts)
|
||||
.set({
|
||||
companyId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(contacts.id, contactId))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlink company from contact
|
||||
*/
|
||||
export const unlinkCompanyFromContact = async (contactId, emailAccountId) => {
|
||||
const contact = await getContactById(contactId, emailAccountId);
|
||||
|
||||
const [updated] = await db
|
||||
.update(contacts)
|
||||
.set({
|
||||
companyId: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(contacts.id, contactId))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create company from contact
|
||||
* Creates a new company using contact's information and links it
|
||||
*/
|
||||
export const createCompanyFromContact = async (contactId, emailAccountId, userId, companyData = {}) => {
|
||||
const contact = await getContactById(contactId, emailAccountId);
|
||||
|
||||
// Check if company with same name already exists
|
||||
if (companyData.name) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.name, companyData.name))
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictError('Firma s týmto názvom už existuje');
|
||||
}
|
||||
}
|
||||
|
||||
// Create company with contact's data as defaults
|
||||
const [newCompany] = await db
|
||||
.insert(companies)
|
||||
.values({
|
||||
name: companyData.name || contact.name || contact.email.split('@')[0],
|
||||
email: companyData.email || contact.email,
|
||||
phone: companyData.phone || null,
|
||||
address: companyData.address || null,
|
||||
city: companyData.city || null,
|
||||
country: companyData.country || null,
|
||||
website: companyData.website || null,
|
||||
description: companyData.description || null,
|
||||
createdBy: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Link contact to newly created company
|
||||
const [updatedContact] = await db
|
||||
.update(contacts)
|
||||
.set({
|
||||
companyId: newCompany.id,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(contacts.id, contactId))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
company: newCompany,
|
||||
contact: updatedContact,
|
||||
};
|
||||
};
|
||||
|
||||
349
src/services/note.service.js
Normal file
349
src/services/note.service.js
Normal file
@@ -0,0 +1,349 @@
|
||||
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 { 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
|
||||
*/
|
||||
export const getAllNotes = async (filters = {}) => {
|
||||
const { searchTerm, companyId, projectId, todoId, contactId } = filters;
|
||||
|
||||
let query = db.select().from(notes);
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (searchTerm) {
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(notes.title, `%${searchTerm}%`),
|
||||
ilike(notes.content, `%${searchTerm}%`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (companyId) {
|
||||
conditions.push(eq(notes.companyId, companyId));
|
||||
}
|
||||
|
||||
if (projectId) {
|
||||
conditions.push(eq(notes.projectId, projectId));
|
||||
}
|
||||
|
||||
if (todoId) {
|
||||
conditions.push(eq(notes.todoId, todoId));
|
||||
}
|
||||
|
||||
if (contactId) {
|
||||
conditions.push(eq(notes.contactId, contactId));
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = query.where(and(...conditions));
|
||||
}
|
||||
|
||||
const result = await query.orderBy(desc(notes.createdAt));
|
||||
return result.map(mapNoteForFrontend);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get note by ID
|
||||
*/
|
||||
export const getNoteById = async (noteId) => {
|
||||
const [note] = await db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(eq(notes.id, noteId))
|
||||
.limit(1);
|
||||
|
||||
if (!note) {
|
||||
throw new NotFoundError('Poznámka nenájdená');
|
||||
}
|
||||
|
||||
return mapNoteForFrontend(note);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new note
|
||||
*/
|
||||
export const createNote = async (userId, data) => {
|
||||
const { title, content, companyId, projectId, todoId, contactId, reminderDate } = data;
|
||||
|
||||
// Verify company exists if provided
|
||||
if (companyId) {
|
||||
const [company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, companyId))
|
||||
.limit(1);
|
||||
|
||||
if (!company) {
|
||||
throw new NotFoundError('Firma nenájdená');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify project exists if provided
|
||||
if (projectId) {
|
||||
const [project] = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.limit(1);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError('Projekt nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify todo exists if provided
|
||||
if (todoId) {
|
||||
const [todo] = await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.id, todoId))
|
||||
.limit(1);
|
||||
|
||||
if (!todo) {
|
||||
throw new NotFoundError('Todo nenájdené');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify contact exists if provided
|
||||
if (contactId) {
|
||||
const [contact] = await db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(eq(contacts.id, contactId))
|
||||
.limit(1);
|
||||
|
||||
if (!contact) {
|
||||
throw new NotFoundError('Kontakt nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
const [newNote] = await db
|
||||
.insert(notes)
|
||||
.values({
|
||||
title: title || null,
|
||||
content,
|
||||
companyId: companyId || null,
|
||||
projectId: projectId || null,
|
||||
todoId: todoId || null,
|
||||
contactId: contactId || null,
|
||||
reminderDate: reminderDate ? new Date(reminderDate) : null,
|
||||
reminderSent: false,
|
||||
createdBy: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return mapNoteForFrontend(newNote);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update note
|
||||
*/
|
||||
export const updateNote = async (noteId, data) => {
|
||||
const note = await getNoteById(noteId);
|
||||
|
||||
const { title, content, companyId, projectId, todoId, contactId, reminderDate } = data;
|
||||
|
||||
// Verify company exists if being changed
|
||||
if (companyId !== undefined && companyId !== null && companyId !== note.companyId) {
|
||||
const [company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, companyId))
|
||||
.limit(1);
|
||||
|
||||
if (!company) {
|
||||
throw new NotFoundError('Firma nenájdená');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify project exists if being changed
|
||||
if (projectId !== undefined && projectId !== null && projectId !== note.projectId) {
|
||||
const [project] = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.limit(1);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError('Projekt nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify todo exists if being changed
|
||||
if (todoId !== undefined && todoId !== null && todoId !== note.todoId) {
|
||||
const [todo] = await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.id, todoId))
|
||||
.limit(1);
|
||||
|
||||
if (!todo) {
|
||||
throw new NotFoundError('Todo nenájdené');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify contact exists if being changed
|
||||
if (contactId !== undefined && contactId !== null && contactId !== note.contactId) {
|
||||
const [contact] = await db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(eq(contacts.id, contactId))
|
||||
.limit(1);
|
||||
|
||||
if (!contact) {
|
||||
throw new NotFoundError('Kontakt nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(notes)
|
||||
.set({
|
||||
title: title !== undefined ? title : note.title,
|
||||
content: content !== undefined ? content : note.content,
|
||||
companyId: companyId !== undefined ? companyId : note.companyId,
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete note
|
||||
*/
|
||||
export const deleteNote = async (noteId) => {
|
||||
await getNoteById(noteId); // Check if exists
|
||||
|
||||
await db.delete(notes).where(eq(notes.id, noteId));
|
||||
|
||||
return { success: true, message: 'Poznámka bola odstránená' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get notes by company ID
|
||||
*/
|
||||
export const getNotesByCompanyId = async (companyId) => {
|
||||
const result = 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
|
||||
.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
|
||||
.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
|
||||
.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);
|
||||
};
|
||||
379
src/services/project.service.js
Normal file
379
src/services/project.service.js
Normal file
@@ -0,0 +1,379 @@
|
||||
import { db } from '../config/database.js';
|
||||
import { projects, todos, notes, timesheets, companies, projectUsers, users } from '../db/schema.js';
|
||||
import { eq, desc, ilike, or, and } from 'drizzle-orm';
|
||||
import { NotFoundError, ConflictError } from '../utils/errors.js';
|
||||
|
||||
/**
|
||||
* Get all projects
|
||||
* Optionally filter by search term or company
|
||||
*/
|
||||
export const getAllProjects = async (searchTerm = null, companyId = null) => {
|
||||
let query = db.select().from(projects);
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (searchTerm) {
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(projects.name, `%${searchTerm}%`),
|
||||
ilike(projects.description, `%${searchTerm}%`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (companyId) {
|
||||
conditions.push(eq(projects.companyId, companyId));
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = query.where(and(...conditions));
|
||||
}
|
||||
|
||||
const result = await query.orderBy(desc(projects.createdAt));
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get project by ID
|
||||
*/
|
||||
export const getProjectById = async (projectId) => {
|
||||
const [project] = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.limit(1);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError('Projekt nenájdený');
|
||||
}
|
||||
|
||||
return project;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new project
|
||||
*/
|
||||
export const createProject = async (userId, data) => {
|
||||
const { name, description, companyId, status, startDate, endDate } = data;
|
||||
|
||||
// If companyId is provided, verify company exists
|
||||
if (companyId) {
|
||||
const [company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, companyId))
|
||||
.limit(1);
|
||||
|
||||
if (!company) {
|
||||
throw new NotFoundError('Firma nenájdená');
|
||||
}
|
||||
}
|
||||
|
||||
const [newProject] = await db
|
||||
.insert(projects)
|
||||
.values({
|
||||
name,
|
||||
description: description || null,
|
||||
companyId: companyId || null,
|
||||
status: status || 'active',
|
||||
startDate: startDate ? new Date(startDate) : null,
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
createdBy: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newProject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update project
|
||||
*/
|
||||
export const updateProject = async (projectId, data) => {
|
||||
const project = await getProjectById(projectId);
|
||||
|
||||
const { name, description, companyId, status, startDate, endDate } = data;
|
||||
|
||||
// If companyId is being changed, verify new company exists
|
||||
if (companyId !== undefined && companyId !== null && companyId !== project.companyId) {
|
||||
const [company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, companyId))
|
||||
.limit(1);
|
||||
|
||||
if (!company) {
|
||||
throw new NotFoundError('Firma nenájdená');
|
||||
}
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(projects)
|
||||
.set({
|
||||
name: name !== undefined ? name : project.name,
|
||||
description: description !== undefined ? description : project.description,
|
||||
companyId: companyId !== undefined ? companyId : project.companyId,
|
||||
status: status !== undefined ? status : project.status,
|
||||
startDate: startDate !== undefined ? (startDate ? new Date(startDate) : null) : project.startDate,
|
||||
endDate: endDate !== undefined ? (endDate ? new Date(endDate) : null) : project.endDate,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(projects.id, projectId))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete project
|
||||
*/
|
||||
export const deleteProject = async (projectId) => {
|
||||
await getProjectById(projectId); // Check if exists
|
||||
|
||||
await db.delete(projects).where(eq(projects.id, projectId));
|
||||
|
||||
return { success: true, message: 'Projekt bol odstránený' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get project with related data (todos, notes, timesheets)
|
||||
*/
|
||||
export const getProjectWithRelations = async (projectId) => {
|
||||
const project = await getProjectById(projectId);
|
||||
|
||||
// Get company if exists
|
||||
let company = null;
|
||||
if (project.companyId) {
|
||||
[company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, project.companyId))
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
// Get related todos
|
||||
const projectTodos = await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.projectId, projectId))
|
||||
.orderBy(desc(todos.createdAt));
|
||||
|
||||
// Get related notes
|
||||
const projectNotes = await db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(eq(notes.projectId, projectId))
|
||||
.orderBy(desc(notes.createdAt));
|
||||
|
||||
// Get related timesheets
|
||||
const projectTimesheets = await db
|
||||
.select()
|
||||
.from(timesheets)
|
||||
.where(eq(timesheets.projectId, projectId))
|
||||
.orderBy(desc(timesheets.uploadedAt));
|
||||
|
||||
// Get assigned users (team members)
|
||||
const rawUsers = await db
|
||||
.select()
|
||||
.from(projectUsers)
|
||||
.leftJoin(users, eq(projectUsers.userId, users.id))
|
||||
.where(eq(projectUsers.projectId, projectId))
|
||||
.orderBy(desc(projectUsers.addedAt));
|
||||
|
||||
const assignedUsers = rawUsers.map((row) => ({
|
||||
id: row.project_users.id,
|
||||
userId: row.project_users.userId,
|
||||
role: row.project_users.role,
|
||||
addedBy: row.project_users.addedBy,
|
||||
addedAt: row.project_users.addedAt,
|
||||
user: row.users ? {
|
||||
id: row.users.id,
|
||||
username: row.users.username,
|
||||
email: row.users.email,
|
||||
role: row.users.role,
|
||||
} : null,
|
||||
}));
|
||||
|
||||
return {
|
||||
...project,
|
||||
company,
|
||||
todos: projectTodos,
|
||||
notes: projectNotes,
|
||||
timesheets: projectTimesheets,
|
||||
assignedUsers,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get projects by company ID
|
||||
*/
|
||||
export const getProjectsByCompanyId = async (companyId) => {
|
||||
return await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.companyId, companyId))
|
||||
.orderBy(desc(projects.createdAt));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get project users (team members)
|
||||
*/
|
||||
export const getProjectUsers = async (projectId) => {
|
||||
await getProjectById(projectId); // Verify project exists
|
||||
|
||||
const rawResults = await db
|
||||
.select()
|
||||
.from(projectUsers)
|
||||
.leftJoin(users, eq(projectUsers.userId, users.id))
|
||||
.where(eq(projectUsers.projectId, projectId))
|
||||
.orderBy(desc(projectUsers.addedAt));
|
||||
|
||||
const assignedUsers = rawResults.map((row) => ({
|
||||
id: row.project_users.id,
|
||||
userId: row.project_users.userId,
|
||||
role: row.project_users.role,
|
||||
addedBy: row.project_users.addedBy,
|
||||
addedAt: row.project_users.addedAt,
|
||||
user: row.users ? {
|
||||
id: row.users.id,
|
||||
username: row.users.username,
|
||||
email: row.users.email,
|
||||
role: row.users.role,
|
||||
} : null,
|
||||
}));
|
||||
|
||||
return assignedUsers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign user to project
|
||||
*/
|
||||
export const assignUserToProject = async (projectId, userId, addedByUserId, role = null) => {
|
||||
await getProjectById(projectId); // Verify project exists
|
||||
|
||||
// Verify user exists
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError('Používateľ nenájdený');
|
||||
}
|
||||
|
||||
// Check if user is already assigned
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(projectUsers)
|
||||
.where(and(eq(projectUsers.projectId, projectId), eq(projectUsers.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictError('Používateľ je už priradený k projektu');
|
||||
}
|
||||
|
||||
// Assign user to project
|
||||
const [assignment] = await db
|
||||
.insert(projectUsers)
|
||||
.values({
|
||||
projectId,
|
||||
userId,
|
||||
role: role || null,
|
||||
addedBy: addedByUserId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Return with user details
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(projectUsers)
|
||||
.leftJoin(users, eq(projectUsers.userId, users.id))
|
||||
.where(eq(projectUsers.id, assignment.id))
|
||||
.limit(1);
|
||||
|
||||
return {
|
||||
id: row.project_users.id,
|
||||
userId: row.project_users.userId,
|
||||
role: row.project_users.role,
|
||||
addedBy: row.project_users.addedBy,
|
||||
addedAt: row.project_users.addedAt,
|
||||
user: row.users ? {
|
||||
id: row.users.id,
|
||||
username: row.users.username,
|
||||
email: row.users.email,
|
||||
role: row.users.role,
|
||||
} : null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove user from project
|
||||
*/
|
||||
export const removeUserFromProject = async (projectId, userId) => {
|
||||
await getProjectById(projectId); // Verify project exists
|
||||
|
||||
// Check if user is assigned
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(projectUsers)
|
||||
.where(and(eq(projectUsers.projectId, projectId), eq(projectUsers.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Používateľ nie je priradený k projektu');
|
||||
}
|
||||
|
||||
// Remove assignment
|
||||
await db
|
||||
.delete(projectUsers)
|
||||
.where(and(eq(projectUsers.projectId, projectId), eq(projectUsers.userId, userId)));
|
||||
|
||||
return { success: true, message: 'Používateľ bol odstránený z projektu' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Update user role on project
|
||||
*/
|
||||
export const updateUserRoleOnProject = async (projectId, userId, role) => {
|
||||
await getProjectById(projectId); // Verify project exists
|
||||
|
||||
// Check if user is assigned
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(projectUsers)
|
||||
.where(and(eq(projectUsers.projectId, projectId), eq(projectUsers.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundError('Používateľ nie je priradený k projektu');
|
||||
}
|
||||
|
||||
// Update role
|
||||
const [updated] = await db
|
||||
.update(projectUsers)
|
||||
.set({ role: role || null })
|
||||
.where(and(eq(projectUsers.projectId, projectId), eq(projectUsers.userId, userId)))
|
||||
.returning();
|
||||
|
||||
// Return with user details
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(projectUsers)
|
||||
.leftJoin(users, eq(projectUsers.userId, users.id))
|
||||
.where(eq(projectUsers.id, updated.id))
|
||||
.limit(1);
|
||||
|
||||
return {
|
||||
id: row.project_users.id,
|
||||
userId: row.project_users.userId,
|
||||
role: row.project_users.role,
|
||||
addedBy: row.project_users.addedBy,
|
||||
addedAt: row.project_users.addedAt,
|
||||
user: row.users ? {
|
||||
id: row.users.id,
|
||||
username: row.users.username,
|
||||
email: row.users.email,
|
||||
role: row.users.role,
|
||||
} : null,
|
||||
};
|
||||
};
|
||||
304
src/services/todo.service.js
Normal file
304
src/services/todo.service.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import { db } from '../config/database.js';
|
||||
import { todos, notes, projects, companies, users } from '../db/schema.js';
|
||||
import { eq, desc, ilike, or, and } from 'drizzle-orm';
|
||||
import { NotFoundError } from '../utils/errors.js';
|
||||
|
||||
/**
|
||||
* Get all todos
|
||||
* Optionally filter by search, project, company, assigned user, or status
|
||||
*/
|
||||
export const getAllTodos = async (filters = {}) => {
|
||||
const { searchTerm, projectId, companyId, assignedTo, status } = filters;
|
||||
|
||||
let query = db.select().from(todos);
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (searchTerm) {
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(todos.title, `%${searchTerm}%`),
|
||||
ilike(todos.description, `%${searchTerm}%`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (projectId) {
|
||||
conditions.push(eq(todos.projectId, projectId));
|
||||
}
|
||||
|
||||
if (companyId) {
|
||||
conditions.push(eq(todos.companyId, companyId));
|
||||
}
|
||||
|
||||
if (assignedTo) {
|
||||
conditions.push(eq(todos.assignedTo, assignedTo));
|
||||
}
|
||||
|
||||
if (status) {
|
||||
conditions.push(eq(todos.status, status));
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = query.where(and(...conditions));
|
||||
}
|
||||
|
||||
const result = await query.orderBy(desc(todos.createdAt));
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get todo by ID
|
||||
*/
|
||||
export const getTodoById = async (todoId) => {
|
||||
const [todo] = await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.id, todoId))
|
||||
.limit(1);
|
||||
|
||||
if (!todo) {
|
||||
throw new NotFoundError('Todo nenájdené');
|
||||
}
|
||||
|
||||
return todo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new todo
|
||||
*/
|
||||
export const createTodo = async (userId, data) => {
|
||||
const { title, description, projectId, companyId, assignedTo, status, priority, dueDate } = data;
|
||||
|
||||
// Verify project exists if provided
|
||||
if (projectId) {
|
||||
const [project] = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.limit(1);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError('Projekt nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify company exists if provided
|
||||
if (companyId) {
|
||||
const [company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, companyId))
|
||||
.limit(1);
|
||||
|
||||
if (!company) {
|
||||
throw new NotFoundError('Firma nenájdená');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify assigned user exists if provided
|
||||
if (assignedTo) {
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, assignedTo))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError('Používateľ nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
const [newTodo] = await db
|
||||
.insert(todos)
|
||||
.values({
|
||||
title,
|
||||
description: description || null,
|
||||
projectId: projectId || null,
|
||||
companyId: companyId || null,
|
||||
assignedTo: assignedTo || null,
|
||||
status: status || 'pending',
|
||||
priority: priority || 'medium',
|
||||
dueDate: dueDate ? new Date(dueDate) : null,
|
||||
createdBy: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newTodo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update todo
|
||||
*/
|
||||
export const updateTodo = async (todoId, data) => {
|
||||
const todo = await getTodoById(todoId);
|
||||
|
||||
const { title, description, projectId, companyId, assignedTo, status, priority, dueDate } = data;
|
||||
|
||||
// Verify project exists if being changed
|
||||
if (projectId !== undefined && projectId !== null && projectId !== todo.projectId) {
|
||||
const [project] = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.limit(1);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError('Projekt nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify company exists if being changed
|
||||
if (companyId !== undefined && companyId !== null && companyId !== todo.companyId) {
|
||||
const [company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, companyId))
|
||||
.limit(1);
|
||||
|
||||
if (!company) {
|
||||
throw new NotFoundError('Firma nenájdená');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify assigned user exists if being changed
|
||||
if (assignedTo !== undefined && assignedTo !== null && assignedTo !== todo.assignedTo) {
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, assignedTo))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError('Používateľ nenájdený');
|
||||
}
|
||||
}
|
||||
|
||||
// Set completedAt when status is changed to 'completed'
|
||||
let completedAt = todo.completedAt;
|
||||
if (status === 'completed' && todo.status !== 'completed') {
|
||||
completedAt = new Date();
|
||||
} else if (status && status !== 'completed') {
|
||||
completedAt = null;
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(todos)
|
||||
.set({
|
||||
title: title !== undefined ? title : todo.title,
|
||||
description: description !== undefined ? description : todo.description,
|
||||
projectId: projectId !== undefined ? projectId : todo.projectId,
|
||||
companyId: companyId !== undefined ? companyId : todo.companyId,
|
||||
assignedTo: assignedTo !== undefined ? assignedTo : todo.assignedTo,
|
||||
status: status !== undefined ? status : todo.status,
|
||||
priority: priority !== undefined ? priority : todo.priority,
|
||||
dueDate: dueDate !== undefined ? (dueDate ? new Date(dueDate) : null) : todo.dueDate,
|
||||
completedAt,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(todos.id, todoId))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete todo
|
||||
*/
|
||||
export const deleteTodo = async (todoId) => {
|
||||
await getTodoById(todoId); // Check if exists
|
||||
|
||||
await db.delete(todos).where(eq(todos.id, todoId));
|
||||
|
||||
return { success: true, message: 'Todo bolo odstránené' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get todo with related data (notes, project, company, assigned user)
|
||||
*/
|
||||
export const getTodoWithRelations = async (todoId) => {
|
||||
const todo = await getTodoById(todoId);
|
||||
|
||||
// Get project if exists
|
||||
let project = null;
|
||||
if (todo.projectId) {
|
||||
[project] = await db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, todo.projectId))
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
// Get company if exists
|
||||
let company = null;
|
||||
if (todo.companyId) {
|
||||
[company] = await db
|
||||
.select()
|
||||
.from(companies)
|
||||
.where(eq(companies.id, todo.companyId))
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
// Get assigned user if exists
|
||||
let assignedUser = null;
|
||||
if (todo.assignedTo) {
|
||||
[assignedUser] = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
firstName: users.firstName,
|
||||
lastName: users.lastName,
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, todo.assignedTo))
|
||||
.limit(1);
|
||||
}
|
||||
|
||||
// Get related notes
|
||||
const todoNotes = await db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(eq(notes.todoId, todoId))
|
||||
.orderBy(desc(notes.createdAt));
|
||||
|
||||
return {
|
||||
...todo,
|
||||
project,
|
||||
company,
|
||||
assignedUser,
|
||||
notes: todoNotes,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get todos by project ID
|
||||
*/
|
||||
export const getTodosByProjectId = async (projectId) => {
|
||||
return await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.projectId, projectId))
|
||||
.orderBy(desc(todos.createdAt));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get todos by company ID
|
||||
*/
|
||||
export const getTodosByCompanyId = async (companyId) => {
|
||||
return await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.companyId, companyId))
|
||||
.orderBy(desc(todos.createdAt));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get todos assigned to a user
|
||||
*/
|
||||
export const getTodosByUserId = async (userId) => {
|
||||
return await db
|
||||
.select()
|
||||
.from(todos)
|
||||
.where(eq(todos.assignedTo, userId))
|
||||
.orderBy(desc(todos.createdAt));
|
||||
};
|
||||
Reference in New Issue
Block a user