Add Time Tracking backend API

Implementovaný kompletný backend pre time tracking:
- Nová tabuľka time_entries s foreign keys na users, projects, todos, companies
- Service layer s business logikou pre CRUD operácie
- Controller pre všetky endpointy
- Validačné schémy pomocou Zod
- Routes s autentifikáciou a validáciou
- Endpointy:
  * POST /api/time-tracking/start - Spustenie timeru
  * POST /api/time-tracking/:id/stop - Zastavenie timeru
  * GET /api/time-tracking/running - Získanie bežiaceho záznamu
  * GET /api/time-tracking/month/:year/:month - Mesačné záznamy
  * GET /api/time-tracking/stats/monthly/:year/:month - Mesačné štatistiky
  * PATCH /api/time-tracking/:id - Aktualizácia záznamu
  * DELETE /api/time-tracking/:id - Zmazanie záznamu
- Podpora pre isEdited flag pri editácii
- Kalkulácia duration v minútach

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-11-24 06:41:39 +01:00
parent ca93b6f2d2
commit 540c1719d3
7 changed files with 843 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
import * as timeTrackingService from '../services/time-tracking.service.js';
import { formatErrorResponse } from '../utils/errors.js';
/**
* Start a new time entry
* POST /api/time-tracking/start
*/
export const startTimeEntry = async (req, res) => {
try {
const userId = req.userId;
const { projectId, todoId, companyId, description } = req.body;
const entry = await timeTrackingService.startTimeEntry(userId, {
projectId,
todoId,
companyId,
description,
});
res.status(201).json({
success: true,
data: entry,
message: 'Časovač bol spustený',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Stop a running time entry
* POST /api/time-tracking/:entryId/stop
*/
export const stopTimeEntry = async (req, res) => {
try {
const userId = req.userId;
const { entryId } = req.params;
const { projectId, todoId, companyId, description } = req.body;
const entry = await timeTrackingService.stopTimeEntry(entryId, userId, {
projectId,
todoId,
companyId,
description,
});
res.status(200).json({
success: true,
data: entry,
message: 'Časovač bol zastavený',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get running time entry for current user
* GET /api/time-tracking/running
*/
export const getRunningTimeEntry = async (req, res) => {
try {
const userId = req.userId;
const entry = await timeTrackingService.getRunningTimeEntry(userId);
res.status(200).json({
success: true,
data: entry,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get all time entries for current user with filters
* GET /api/time-tracking?projectId=xxx&todoId=xxx&companyId=xxx&startDate=xxx&endDate=xxx
*/
export const getAllTimeEntries = async (req, res) => {
try {
const userId = req.userId;
const { projectId, todoId, companyId, startDate, endDate } = req.query;
const filters = {
projectId,
todoId,
companyId,
startDate,
endDate,
};
const entries = await timeTrackingService.getAllTimeEntries(userId, filters);
res.status(200).json({
success: true,
count: entries.length,
data: entries,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get time entries for a specific month
* GET /api/time-tracking/month/:year/:month
*/
export const getMonthlyTimeEntries = async (req, res) => {
try {
const userId = req.userId;
const { year, month } = req.params;
const entries = await timeTrackingService.getMonthlyTimeEntries(
userId,
parseInt(year),
parseInt(month)
);
res.status(200).json({
success: true,
count: entries.length,
data: entries,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get time entry by ID
* GET /api/time-tracking/:entryId
*/
export const getTimeEntryById = async (req, res) => {
try {
const { entryId } = req.params;
const entry = await timeTrackingService.getTimeEntryById(entryId);
res.status(200).json({
success: true,
data: entry,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get time entry with related data
* GET /api/time-tracking/:entryId/details
*/
export const getTimeEntryWithRelations = async (req, res) => {
try {
const { entryId } = req.params;
const entry = await timeTrackingService.getTimeEntryWithRelations(entryId);
res.status(200).json({
success: true,
data: entry,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Update time entry
* PATCH /api/time-tracking/:entryId
*/
export const updateTimeEntry = async (req, res) => {
try {
const userId = req.userId;
const { entryId } = req.params;
const { startTime, endTime, projectId, todoId, companyId, description } = req.body;
const entry = await timeTrackingService.updateTimeEntry(entryId, userId, {
startTime,
endTime,
projectId,
todoId,
companyId,
description,
});
res.status(200).json({
success: true,
data: entry,
message: 'Záznam bol aktualizovaný',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Delete time entry
* DELETE /api/time-tracking/:entryId
*/
export const deleteTimeEntry = async (req, res) => {
try {
const userId = req.userId;
const { entryId } = req.params;
const result = await timeTrackingService.deleteTimeEntry(entryId, userId);
res.status(200).json(result);
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get monthly statistics
* GET /api/time-tracking/stats/monthly/:year/:month
*/
export const getMonthlyStats = async (req, res) => {
try {
const userId = req.userId;
const { year, month } = req.params;
const stats = await timeTrackingService.getMonthlyStats(
userId,
parseInt(year),
parseInt(month)
);
res.status(200).json({
success: true,
data: stats,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};