refractoring & add timesheet service

This commit is contained in:
richardtekula
2025-11-25 07:52:31 +01:00
parent 125e30338a
commit 31297ee9a9
13 changed files with 277 additions and 463 deletions

View File

@@ -1,97 +1,42 @@
import { db } from '../config/database.js';
import { timesheets, users } from '../db/schema.js';
import { eq, and, desc } from 'drizzle-orm';
import { formatErrorResponse, NotFoundError, BadRequestError, ForbiddenError } from '../utils/errors.js';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import * as timesheetService from '../services/timesheet.service.js';
import { formatErrorResponse } from '../utils/errors.js';
/**
* Upload timesheet
* POST /api/timesheets/upload
*/
export const uploadTimesheet = async (req, res) => {
const { year, month } = req.body;
const userId = req.userId;
const file = req.file;
let savedFilePath = null;
try {
if (!file) {
throw new BadRequestError('Súbor nebol nahraný');
const { year, month, userId: requestUserId } = req.body;
// Determine target userId:
// - If requestUserId is provided and user is admin, use requestUserId
// - Otherwise, use req.userId (upload for themselves)
let targetUserId = req.userId;
if (requestUserId) {
if (req.user.role !== 'admin') {
const errorResponse = formatErrorResponse(
new Error('Iba admin môže nahrávať timesheets za iných používateľov'),
process.env.NODE_ENV === 'development'
);
return res.status(403).json(errorResponse);
}
targetUserId = requestUserId;
}
// Validate file type
const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'];
if (!allowedTypes.includes(file.mimetype)) {
throw new BadRequestError('Neplatný typ súboru. Povolené sú iba PDF a Excel súbory.');
}
// Determine file type
let fileType = 'pdf';
if (file.mimetype.includes('sheet') || file.mimetype.includes('excel')) {
fileType = 'xlsx';
}
// Create directory structure: uploads/timesheets/{userId}/{year}/{month}
const uploadsDir = path.join(process.cwd(), 'uploads', 'timesheets');
const userDir = path.join(uploadsDir, userId, year.toString(), month.toString());
await fs.mkdir(userDir, { recursive: true });
// Generate unique filename
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
const name = path.basename(file.originalname, ext);
const filename = `${name}-${uniqueSuffix}${ext}`;
savedFilePath = path.join(userDir, filename);
// Save file from memory buffer to disk
await fs.writeFile(savedFilePath, file.buffer);
// Create timesheet record
const [newTimesheet] = await db
.insert(timesheets)
.values({
userId,
fileName: file.originalname,
filePath: savedFilePath,
fileType,
fileSize: file.size,
year: parseInt(year),
month: parseInt(month),
isGenerated: false,
})
.returning();
const timesheet = await timesheetService.uploadTimesheet({
userId: targetUserId,
year,
month,
file: req.file,
});
res.status(201).json({
success: true,
data: {
timesheet: {
id: newTimesheet.id,
fileName: newTimesheet.fileName,
fileType: newTimesheet.fileType,
fileSize: newTimesheet.fileSize,
year: newTimesheet.year,
month: newTimesheet.month,
isGenerated: newTimesheet.isGenerated,
uploadedAt: newTimesheet.uploadedAt,
},
},
data: { timesheet },
message: 'Timesheet bol úspešne nahraný',
});
} catch (error) {
// If error occurs and file was saved, delete it
if (savedFilePath) {
try {
await fs.unlink(savedFilePath);
} catch (unlinkError) {
console.error('Failed to delete file:', unlinkError);
}
}
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
@@ -102,34 +47,10 @@ export const uploadTimesheet = async (req, res) => {
* GET /api/timesheets/my
*/
export const getMyTimesheets = async (req, res) => {
const userId = req.userId;
const { year, month } = req.query;
try {
let conditions = [eq(timesheets.userId, userId)];
const { year, month } = req.query;
if (year) {
conditions.push(eq(timesheets.year, parseInt(year)));
}
if (month) {
conditions.push(eq(timesheets.month, parseInt(month)));
}
const userTimesheets = await 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));
const userTimesheets = await timesheetService.getTimesheetsForUser(req.userId, { year, month });
res.status(200).json({
success: true,
@@ -149,42 +70,10 @@ export const getMyTimesheets = async (req, res) => {
* GET /api/timesheets/all
*/
export const getAllTimesheets = async (req, res) => {
const { userId: filterUserId, year, month } = req.query;
try {
let conditions = [];
const { userId, year, month } = req.query;
if (filterUserId) {
conditions.push(eq(timesheets.userId, filterUserId));
}
if (year) {
conditions.push(eq(timesheets.year, parseInt(year)));
}
if (month) {
conditions.push(eq(timesheets.month, parseInt(month)));
}
const allTimesheets = await 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));
const allTimesheets = await timesheetService.getAllTimesheets({ userId, year, month });
res.status(200).json({
success: true,
@@ -204,35 +93,14 @@ export const getAllTimesheets = async (req, res) => {
* GET /api/timesheets/:timesheetId/download
*/
export const downloadTimesheet = async (req, res) => {
const { timesheetId } = req.params;
const userId = req.userId;
const userRole = req.user.role; // Fix: use req.user.role instead of req.userRole
try {
const [timesheet] = await db
.select()
.from(timesheets)
.where(eq(timesheets.id, timesheetId))
.limit(1);
const { timesheetId } = req.params;
const { filePath, fileName } = await timesheetService.getDownloadInfo(timesheetId, {
userId: req.userId,
role: req.user.role,
});
if (!timesheet) {
throw new NotFoundError('Timesheet nenájdený');
}
// Check permissions: user can only download their own timesheets, admin can download all
if (userRole !== 'admin' && timesheet.userId !== userId) {
throw new ForbiddenError('Nemáte oprávnenie stiahnuť tento timesheet');
}
// Check if file exists
try {
await fs.access(timesheet.filePath);
} catch {
throw new NotFoundError('Súbor nebol nájdený na serveri');
}
// Send file
res.download(timesheet.filePath, timesheet.fileName);
res.download(filePath, fileName);
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
@@ -244,36 +112,13 @@ export const downloadTimesheet = async (req, res) => {
* DELETE /api/timesheets/:timesheetId
*/
export const deleteTimesheet = async (req, res) => {
const { timesheetId } = req.params;
const userId = req.userId;
const userRole = req.user.role; // Fix: use req.user.role instead of req.userRole
try {
const [timesheet] = await db
.select()
.from(timesheets)
.where(eq(timesheets.id, timesheetId))
.limit(1);
const { timesheetId } = req.params;
if (!timesheet) {
throw new NotFoundError('Timesheet nenájdený');
}
// Check permissions: user can only delete their own timesheets, admin can delete all
if (userRole !== 'admin' && timesheet.userId !== userId) {
throw new ForbiddenError('Nemáte oprávnenie zmazať tento timesheet');
}
// Delete file from filesystem
try {
await fs.unlink(timesheet.filePath);
} catch (unlinkError) {
console.error('Failed to delete file from filesystem:', unlinkError);
// Continue with database deletion even if file deletion fails
}
// Delete from database
await db.delete(timesheets).where(eq(timesheets.id, timesheetId));
await timesheetService.deleteTimesheet(timesheetId, {
userId: req.userId,
role: req.user.role,
});
res.status(200).json({
success: true,