import fs from 'fs/promises'; import path from 'path'; import { db } from '../config/database.js'; import { timesheets, users } from '../db/schema.js'; import { and, desc, eq } from 'drizzle-orm'; import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors.js'; const ALLOWED_MIME_TYPES = [ 'application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', ]; const BASE_UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'timesheets'); const sanitizeTimesheet = (record) => ({ id: record.id, fileName: record.fileName, fileType: record.fileType, fileSize: record.fileSize, year: record.year, month: record.month, isGenerated: record.isGenerated, uploadedAt: record.uploadedAt, }); const detectFileType = (mimeType) => { if (!ALLOWED_MIME_TYPES.includes(mimeType)) { throw new BadRequestError('Neplatný typ súboru. Povolené sú iba PDF a Excel súbory.'); } return mimeType.includes('sheet') || mimeType.includes('excel') ? 'xlsx' : 'pdf'; }; const buildDestinationPath = (userId, year, month, originalName) => { const ext = path.extname(originalName); const name = path.basename(originalName, ext); const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`; const filename = `${name}-${uniqueSuffix}${ext}`; const folder = path.join(BASE_UPLOAD_DIR, userId, year.toString(), month.toString()); const filePath = path.join(folder, filename); return { folder, filename, filePath }; }; const ensureTimesheetExists = async (timesheetId) => { const [timesheet] = await db .select() .from(timesheets) .where(eq(timesheets.id, timesheetId)) .limit(1); if (!timesheet) { throw new NotFoundError('Timesheet nenájdený'); } return timesheet; }; const assertAccess = (timesheet, { userId, role }) => { if (role !== 'admin' && timesheet.userId !== userId) { throw new ForbiddenError('Nemáte oprávnenie k tomuto timesheetu'); } }; const safeUnlink = async (filePath) => { if (!filePath) return; try { await fs.unlink(filePath); } catch (error) { // Keep server responsive even if cleanup fails console.error('Nepodarilo sa zmazať súbor:', error); } }; export const uploadTimesheet = async ({ userId, year, month, file }) => { if (!file) { throw new BadRequestError('Súbor nebol nahraný'); } const parsedYear = parseInt(year); const parsedMonth = parseInt(month); const fileType = detectFileType(file.mimetype); const { folder, filename, filePath } = buildDestinationPath(userId, parsedYear, parsedMonth, file.originalname); await fs.mkdir(folder, { recursive: true }); try { await fs.writeFile(filePath, file.buffer); const [newTimesheet] = await db .insert(timesheets) .values({ userId, fileName: file.originalname, filePath, fileType, fileSize: file.size, year: parsedYear, month: parsedMonth, isGenerated: false, }) .returning(); return sanitizeTimesheet(newTimesheet); } catch (error) { await safeUnlink(filePath); throw error; } }; export const getTimesheetsForUser = async (userId, { year, month } = {}) => { const conditions = [eq(timesheets.userId, userId)]; if (year) { conditions.push(eq(timesheets.year, parseInt(year))); } if (month) { conditions.push(eq(timesheets.month, parseInt(month))); } return db .select({ id: timesheets.id, fileName: timesheets.fileName, fileType: timesheets.fileType, fileSize: timesheets.fileSize, year: timesheets.year, month: timesheets.month, isGenerated: timesheets.isGenerated, uploadedAt: timesheets.uploadedAt, }) .from(timesheets) .where(and(...conditions)) .orderBy(desc(timesheets.uploadedAt)); }; export const getAllTimesheets = async ({ userId, year, month } = {}) => { const conditions = []; if (userId) { conditions.push(eq(timesheets.userId, userId)); } if (year) { conditions.push(eq(timesheets.year, parseInt(year))); } if (month) { conditions.push(eq(timesheets.month, parseInt(month))); } return db .select({ id: timesheets.id, fileName: timesheets.fileName, fileType: timesheets.fileType, fileSize: timesheets.fileSize, year: timesheets.year, month: timesheets.month, isGenerated: timesheets.isGenerated, uploadedAt: timesheets.uploadedAt, userId: timesheets.userId, username: users.username, firstName: users.firstName, lastName: users.lastName, }) .from(timesheets) .leftJoin(users, eq(timesheets.userId, users.id)) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(desc(timesheets.uploadedAt)); }; export const getDownloadInfo = async (timesheetId, { userId, role }) => { const timesheet = await ensureTimesheetExists(timesheetId); assertAccess(timesheet, { userId, role }); try { await fs.access(timesheet.filePath); } catch { throw new NotFoundError('Súbor nebol nájdený na serveri'); } return { timesheet, filePath: timesheet.filePath, fileName: timesheet.fileName, }; }; export const deleteTimesheet = async (timesheetId, { userId, role }) => { const timesheet = await ensureTimesheetExists(timesheetId); assertAccess(timesheet, { userId, role }); await safeUnlink(timesheet.filePath); await db.delete(timesheets).where(eq(timesheets.id, timesheetId)); };