refractoring & add timesheet service
This commit is contained in:
@@ -21,7 +21,6 @@ import timesheetRoutes from './routes/timesheet.routes.js';
|
||||
import companyRoutes from './routes/company.routes.js';
|
||||
import projectRoutes from './routes/project.routes.js';
|
||||
import todoRoutes from './routes/todo.routes.js';
|
||||
import noteRoutes from './routes/note.routes.js';
|
||||
import timeTrackingRoutes from './routes/time-tracking.routes.js';
|
||||
|
||||
const app = express();
|
||||
@@ -82,7 +81,6 @@ app.use('/api/timesheets', timesheetRoutes);
|
||||
app.use('/api/companies', companyRoutes);
|
||||
app.use('/api/projects', projectRoutes);
|
||||
app.use('/api/todos', todoRoutes);
|
||||
app.use('/api/notes', noteRoutes);
|
||||
app.use('/api/time-tracking', timeTrackingRoutes);
|
||||
|
||||
// Basic route
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { validateBody } from '../middlewares/security/validateInput.js';
|
||||
import {
|
||||
loginSchema,
|
||||
setPasswordSchema,
|
||||
linkEmailSchema,
|
||||
} from '../validators/auth.validators.js';
|
||||
import {
|
||||
loginRateLimiter,
|
||||
@@ -39,25 +38,10 @@ router.post(
|
||||
authController.setPassword
|
||||
);
|
||||
|
||||
// KROK 3: Link email
|
||||
router.post(
|
||||
'/link-email',
|
||||
authenticate,
|
||||
sensitiveOperationLimiter,
|
||||
validateBody(linkEmailSchema),
|
||||
authController.linkEmail
|
||||
);
|
||||
|
||||
// KROK 3 (alternatíva): Skip email
|
||||
router.post('/skip-email', authenticate, authController.skipEmail);
|
||||
|
||||
// Logout
|
||||
router.post('/logout', authenticate, authController.logout);
|
||||
|
||||
// Get current session
|
||||
router.get('/session', authenticate, authController.getSession);
|
||||
|
||||
// Get current user profile
|
||||
router.get('/me', authenticate, authController.getMe);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -24,13 +24,6 @@ router.get(
|
||||
companyController.getCompanyById
|
||||
);
|
||||
|
||||
// Get company with relations (projects, todos, notes)
|
||||
router.get(
|
||||
'/:companyId/details',
|
||||
validateParams(z.object({ companyId: z.string().uuid() })),
|
||||
companyController.getCompanyWithRelations
|
||||
);
|
||||
|
||||
// Create new company
|
||||
router.post(
|
||||
'/',
|
||||
|
||||
@@ -53,38 +53,4 @@ router.delete(
|
||||
contactController.removeContact
|
||||
);
|
||||
|
||||
// Link company to contact
|
||||
router.post(
|
||||
'/:contactId/link-company',
|
||||
validateParams(z.object({ contactId: z.string().uuid() })),
|
||||
validateBody(z.object({ companyId: z.string().uuid() })),
|
||||
contactController.linkCompanyToContact
|
||||
);
|
||||
|
||||
// Unlink company from contact
|
||||
router.post(
|
||||
'/:contactId/unlink-company',
|
||||
validateParams(z.object({ contactId: z.string().uuid() })),
|
||||
contactController.unlinkCompanyFromContact
|
||||
);
|
||||
|
||||
// Create company from contact
|
||||
router.post(
|
||||
'/:contactId/create-company',
|
||||
validateParams(z.object({ contactId: z.string().uuid() })),
|
||||
validateBody(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
phone: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
website: z.string().url().optional(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
),
|
||||
contactController.createCompanyFromContact
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -42,13 +42,6 @@ router.post(
|
||||
crmEmailController.markThreadRead
|
||||
);
|
||||
|
||||
// Get emails for a specific contact
|
||||
router.get(
|
||||
'/contact/:contactId',
|
||||
validateParams(z.object({ contactId: z.string().uuid() })),
|
||||
crmEmailController.getContactEmails
|
||||
);
|
||||
|
||||
// Mark all emails from contact as read
|
||||
router.post(
|
||||
'/contact/:contactId/read',
|
||||
@@ -56,14 +49,6 @@ router.post(
|
||||
crmEmailController.markContactEmailsRead
|
||||
);
|
||||
|
||||
// Mark email as read/unread
|
||||
router.patch(
|
||||
'/:jmapId/read',
|
||||
validateParams(z.object({ jmapId: z.string() })),
|
||||
validateBody(z.object({ isRead: z.boolean() })),
|
||||
crmEmailController.markAsRead
|
||||
);
|
||||
|
||||
// Send email reply
|
||||
router.post(
|
||||
'/reply',
|
||||
|
||||
@@ -21,13 +21,6 @@ router.use(authenticate);
|
||||
// Get all email accounts for logged-in user
|
||||
router.get('/', emailAccountController.getEmailAccounts);
|
||||
|
||||
// Get specific email account
|
||||
router.get(
|
||||
'/:id',
|
||||
validateParams(z.object({ id: z.string().uuid() })),
|
||||
emailAccountController.getEmailAccount
|
||||
);
|
||||
|
||||
// Create new email account
|
||||
router.post(
|
||||
'/',
|
||||
@@ -36,23 +29,6 @@ router.post(
|
||||
emailAccountController.createEmailAccount
|
||||
);
|
||||
|
||||
// Update email account password
|
||||
router.patch(
|
||||
'/:id/password',
|
||||
validateParams(z.object({ id: z.string().uuid() })),
|
||||
validateBody(z.object({ emailPassword: z.string().min(1) })),
|
||||
sensitiveOperationLimiter,
|
||||
emailAccountController.updateEmailAccountPassword
|
||||
);
|
||||
|
||||
// Toggle email account status
|
||||
router.patch(
|
||||
'/:id/status',
|
||||
validateParams(z.object({ id: z.string().uuid() })),
|
||||
validateBody(z.object({ isActive: z.boolean() })),
|
||||
emailAccountController.toggleEmailAccountStatus
|
||||
);
|
||||
|
||||
// Set email account as primary
|
||||
router.post(
|
||||
'/:id/set-primary',
|
||||
|
||||
@@ -24,13 +24,6 @@ router.get(
|
||||
projectController.getProjectById
|
||||
);
|
||||
|
||||
// Get project with relations (company, todos, notes, timesheets)
|
||||
router.get(
|
||||
'/:projectId/details',
|
||||
validateParams(z.object({ projectId: z.string().uuid() })),
|
||||
projectController.getProjectWithRelations
|
||||
);
|
||||
|
||||
// Create new project
|
||||
router.post(
|
||||
'/',
|
||||
|
||||
@@ -49,6 +49,7 @@ router.post(
|
||||
validateBody(z.object({
|
||||
year: z.string().regex(/^\d{4}$/, 'Rok musí byť 4-miestne číslo'),
|
||||
month: z.string().regex(/^([1-9]|1[0-2])$/, 'Mesiac musí byť číslo od 1 do 12'),
|
||||
userId: z.string().uuid().optional(), // Optional: admin can upload for other users
|
||||
})),
|
||||
timesheetController.uploadTimesheet
|
||||
);
|
||||
|
||||
@@ -17,9 +17,6 @@ router.use(authenticate);
|
||||
// Get all todos
|
||||
router.get('/', todoController.getAllTodos);
|
||||
|
||||
// Get my todos (assigned to current user)
|
||||
router.get('/my', todoController.getMyTodos);
|
||||
|
||||
// Get todo by ID
|
||||
router.get(
|
||||
'/:todoId',
|
||||
@@ -27,13 +24,6 @@ router.get(
|
||||
todoController.getTodoById
|
||||
);
|
||||
|
||||
// Get todo with relations (project, company, assigned user, notes)
|
||||
router.get(
|
||||
'/:todoId/details',
|
||||
validateParams(z.object({ todoId: z.string().uuid() })),
|
||||
todoController.getTodoWithRelations
|
||||
);
|
||||
|
||||
// Create new todo
|
||||
router.post(
|
||||
'/',
|
||||
|
||||
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