refractoring & add timesheet service
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user