import { db } from '../config/database.js'; import { events, eventUsers, users, todos, todoUsers } from '../db/schema.js'; import { eq, and, gte, lt, desc, inArray } from 'drizzle-orm'; import { NotFoundError } from '../utils/errors.js'; /** * Parse datetime-local string to Date object * datetime-local format: "2024-01-15T12:00" (no timezone) * Interprets as Europe/Bratislava timezone */ const parseLocalDateTime = (dateTimeString) => { if (!dateTimeString) return null; // Ak už má timezone info, parsuj priamo if (dateTimeString.includes('Z') || /[+-]\d{2}:\d{2}$/.test(dateTimeString)) { return new Date(dateTimeString); } // Pre datetime-local input (bez timezone) - interpretuj ako Europe/Bratislava let normalized = dateTimeString; if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(dateTimeString)) { normalized = `${dateTimeString}:00`; } const [datePart] = normalized.split('T'); const [year, month, day] = datePart.split('-').map(Number); // Detekcia letného času pre Európu const isDST = month > 3 && month < 10 || (month === 3 && day >= 25) || (month === 10 && day < 25); const offset = isDST ? '+02:00' : '+01:00'; const isoString = `${normalized}${offset}`; return new Date(isoString); }; /** * Format event timestamps to ISO string with timezone */ const formatEventOutput = (event, assignedUsers = []) => { if (!event) return null; return { ...event, start: event.start instanceof Date ? event.start.toISOString() : event.start, end: event.end instanceof Date ? event.end.toISOString() : event.end, createdAt: event.createdAt instanceof Date ? event.createdAt.toISOString() : event.createdAt, updatedAt: event.updatedAt instanceof Date ? event.updatedAt.toISOString() : event.updatedAt, assignedUsers, }; }; /** * Get assigned users for events */ const getAssignedUsersForEvents = async (eventIds) => { if (!eventIds || eventIds.length === 0) return {}; const assignedUsersData = await db .select({ eventId: eventUsers.eventId, userId: users.id, username: users.username, firstName: users.firstName, lastName: users.lastName, }) .from(eventUsers) .innerJoin(users, eq(eventUsers.userId, users.id)) .where(inArray(eventUsers.eventId, eventIds)); // Group by eventId const usersByEventId = {}; for (const row of assignedUsersData) { if (!usersByEventId[row.eventId]) { usersByEventId[row.eventId] = []; } usersByEventId[row.eventId].push({ id: row.userId, username: row.username, firstName: row.firstName, lastName: row.lastName, }); } return usersByEventId; }; /** * Get event IDs accessible to user (member sees only assigned events) */ const getAccessibleEventIds = async (userId) => { const userEvents = await db .select({ eventId: eventUsers.eventId }) .from(eventUsers) .where(eq(eventUsers.userId, userId)); return userEvents.map((row) => row.eventId); }; /** * Get calendar data - events and todos for month * Filtered by user access (admin sees all, member sees only assigned) */ export const getCalendarData = async (year, month, userId, isAdmin) => { // Month boundaries const startOfMonthStr = `${year}-${String(month).padStart(2, '0')}-01T00:00`; const endMonth = month === 12 ? 1 : month + 1; const endYear = month === 12 ? year + 1 : year; const endOfMonthStr = `${endYear}-${String(endMonth).padStart(2, '0')}-01T00:00`; const startOfMonth = parseLocalDateTime(startOfMonthStr); const endOfMonth = parseLocalDateTime(endOfMonthStr); // Get events let monthEvents; if (isAdmin) { // Admin sees all events monthEvents = await db .select() .from(events) .where( and( gte(events.start, startOfMonth), lt(events.start, endOfMonth) ) ) .orderBy(desc(events.start)); } else { // Member sees only assigned events const accessibleEventIds = await getAccessibleEventIds(userId); if (accessibleEventIds.length === 0) { monthEvents = []; } else { monthEvents = await db .select() .from(events) .where( and( gte(events.start, startOfMonth), lt(events.start, endOfMonth), inArray(events.id, accessibleEventIds) ) ) .orderBy(desc(events.start)); } } // Get assigned users for events const eventIds = monthEvents.map((e) => e.id); const usersByEventId = await getAssignedUsersForEvents(eventIds); const formattedEvents = monthEvents.map((event) => formatEventOutput(event, usersByEventId[event.id] || []) ); // Get todos for month (with dueDate in this month) let monthTodos; if (isAdmin) { monthTodos = await db .select() .from(todos) .where( and( gte(todos.dueDate, startOfMonth), lt(todos.dueDate, endOfMonth) ) ) .orderBy(desc(todos.dueDate)); } else { // Member sees only assigned todos const userTodos = await db .select({ todoId: todoUsers.todoId }) .from(todoUsers) .where(eq(todoUsers.userId, userId)); const todoIds = userTodos.map((row) => row.todoId); if (todoIds.length === 0) { monthTodos = []; } else { monthTodos = await db .select() .from(todos) .where( and( gte(todos.dueDate, startOfMonth), lt(todos.dueDate, endOfMonth), inArray(todos.id, todoIds) ) ) .orderBy(desc(todos.dueDate)); } } // Format todos for calendar const formattedTodos = monthTodos.map((todo) => ({ ...todo, dueDate: todo.dueDate instanceof Date ? todo.dueDate.toISOString() : todo.dueDate, createdAt: todo.createdAt instanceof Date ? todo.createdAt.toISOString() : todo.createdAt, updatedAt: todo.updatedAt instanceof Date ? todo.updatedAt.toISOString() : todo.updatedAt, })); return { events: formattedEvents, todos: formattedTodos, }; }; /** * Get event by ID */ export const getEventById = async (eventId, userId, isAdmin) => { const [event] = await db .select() .from(events) .where(eq(events.id, eventId)) .limit(1); if (!event) { throw new NotFoundError('Event nenájdený'); } // Check access for non-admin if (!isAdmin) { const accessibleEventIds = await getAccessibleEventIds(userId); if (!accessibleEventIds.includes(eventId)) { throw new NotFoundError('Event nenájdený'); } } const usersByEventId = await getAssignedUsersForEvents([eventId]); return formatEventOutput(event, usersByEventId[eventId] || []); }; /** * Create a new event */ export const createEvent = async (userId, data) => { const { title, description, type, start, end, assignedUserIds } = data; // Create event const [newEvent] = await db .insert(events) .values({ title, description: description || null, type: type || 'meeting', start: parseLocalDateTime(start), end: parseLocalDateTime(end), createdBy: userId, }) .returning(); // Always assign creator + any additional users const allUserIds = new Set([userId]); if (assignedUserIds && Array.isArray(assignedUserIds)) { assignedUserIds.forEach((id) => allUserIds.add(id)); } // Insert event users const eventUserInserts = Array.from(allUserIds).map((uid) => ({ eventId: newEvent.id, userId: uid, })); await db.insert(eventUsers).values(eventUserInserts); // Get assigned users for response const usersByEventId = await getAssignedUsersForEvents([newEvent.id]); return formatEventOutput(newEvent, usersByEventId[newEvent.id] || []); }; /** * Update an event */ export const updateEvent = async (eventId, data) => { // Check if event exists const [existingEvent] = await db .select() .from(events) .where(eq(events.id, eventId)) .limit(1); if (!existingEvent) { throw new NotFoundError('Event nenájdený'); } const { title, description, type, start, end, assignedUserIds } = data; const updateData = { updatedAt: new Date(), }; if (title !== undefined) updateData.title = title; if (description !== undefined) updateData.description = description; if (type !== undefined) updateData.type = type; if (start !== undefined) updateData.start = parseLocalDateTime(start); if (end !== undefined) updateData.end = parseLocalDateTime(end); const [updated] = await db .update(events) .set(updateData) .where(eq(events.id, eventId)) .returning(); // Update assigned users if provided if (assignedUserIds !== undefined) { // Delete existing assignments await db.delete(eventUsers).where(eq(eventUsers.eventId, eventId)); // Always include creator const allUserIds = new Set([existingEvent.createdBy]); if (Array.isArray(assignedUserIds)) { assignedUserIds.forEach((id) => allUserIds.add(id)); } // Insert new assignments const eventUserInserts = Array.from(allUserIds) .filter((id) => id) // filter out null .map((uid) => ({ eventId: eventId, userId: uid, })); if (eventUserInserts.length > 0) { await db.insert(eventUsers).values(eventUserInserts); } } const usersByEventId = await getAssignedUsersForEvents([eventId]); return formatEventOutput(updated, usersByEventId[eventId] || []); }; /** * Delete an event */ export const deleteEvent = async (eventId) => { const [event] = await db .select() .from(events) .where(eq(events.id, eventId)) .limit(1); if (!event) { throw new NotFoundError('Event nenájdený'); } // eventUsers will be deleted automatically due to CASCADE await db.delete(events).where(eq(events.id, eventId)); return { success: true, message: 'Event bol zmazaný' }; };