Files
crm-server/src/services/timesheet.service.js
richardtekula 176d3c5fec Refactor: Split jmap.service.js into modules and update Slovak translations
- Split 753-line jmap.service.js into modular structure:
  - jmap/config.js: JMAP configuration functions
  - jmap/client.js: Base JMAP requests (jmapRequest, getMailboxes, getIdentities)
  - jmap/discovery.js: Contact discovery from JMAP
  - jmap/search.js: Email search functionality
  - jmap/sync.js: Email synchronization
  - jmap/operations.js: Email operations (markAsRead, sendEmail)
  - jmap/index.js: Re-exports for backward compatibility

- Update all imports across codebase to use new module structure

- Translate remaining English error/log messages to Slovak:
  - email.service.js: JMAP validation messages
  - admin.service.js: Email account creation error
  - audit.service.js: Audit event logging error
  - timesheet.service.js: File deletion error
  - database.js: Database error message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 11:11:41 +01:00

198 lines
5.4 KiB
JavaScript

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));
};