refractoring & add timesheet service
This commit is contained in:
197
src/services/timesheet.service.js
Normal file
197
src/services/timesheet.service.js
Normal file
@@ -0,0 +1,197 @@
|
||||
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('Failed to delete file:', 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));
|
||||
};
|
||||
Reference in New Issue
Block a user