diff --git a/DOKUMENTACIA.md b/DOKUMENTACIA.md index 46adf08..6109806 100644 --- a/DOKUMENTACIA.md +++ b/DOKUMENTACIA.md @@ -561,10 +561,26 @@ getMonthlyStats(userId, year, month) - averagePerDay - byProject (čas per projekt) - byCompany (čas per firma) + +generateMonthlyTimesheet(userId, year, month) + → Generate Excel (XLSX) report z time entries + → Fetch user info: username, firstName, lastName + → Fetch completed entries for month (LEFT JOIN projects, todos, companies) + → Filter: iba entries s endTime a duration + → Počíta: totalMinutes, dailyTotals (per day) + → Vytvára Excel workbook cez ExcelJS: + - Header: Timesheet, Name, Period, Generated date + - Table: Date, Project, Todo, Company, Description, Start, End, Duration + - Summary: Daily totals + Overall total + → Save to: uploads/timesheets/{userId}/{year}/{month}/timesheet-{period}-{timestamp}.xlsx + → INSERT INTO timesheets (isGenerated = true) + → Vráti: { timesheet, filePath, entriesCount, totalMinutes, totalHours } ``` **Volá:** -- Databázu (timeEntries, projects, todos, companies) +- Databázu (timeEntries, projects, todos, companies, users, timesheets) +- ExcelJS library (workbook generation) +- File system (fs/promises) - save XLSX file - `utils/errors.NotFoundError` - `utils/errors.BadRequestError` @@ -609,10 +625,233 @@ logUserCreation(adminId, newUserId, username, role, ip, userAgent) --- +### 13. timesheet.controller.js +**Účel:** File upload a správa timesheetov (PDF/Excel) + +**Databáza:** `timesheets`, `users` + +**Metódy:** +```javascript +uploadTimesheet(req, res) + → Validácia file type (PDF, Excel) + → Max 10MB limit + → Save to: uploads/timesheets/{userId}/{year}/{month}/ + → INSERT INTO timesheets + → File stored on disk (not in DB) + +getMyTimesheets(userId, filters) + → Filter: year, month (optional) + → SELECT * FROM timesheets WHERE userId + → ORDER BY uploadedAt DESC + +getAllTimesheets(filters) + → Admin only! + → Filter: userId, year, month (all optional) + → LEFT JOIN users (get username, name) + → Vráti timesheets všetkých userov + +downloadTimesheet(timesheetId, userId, userRole) + → Check permissions: owner alebo admin + → Validate file exists on disk + → res.download(filePath, fileName) + +deleteTimesheet(timesheetId, userId, userRole) + → Check permissions: owner alebo admin + → Delete file from filesystem (fs.unlink) + → DELETE FROM timesheets + → Continue even if file deletion fails +``` + +**Volá:** +- Databázu (timesheets, users) +- File system operations (fs/promises) +- `utils/errors.NotFoundError, ForbiddenError, BadRequestError` + +**File Storage Pattern:** +``` +uploads/timesheets/ + └── {userId}/ + └── {year}/ + └── {month}/ + └── filename-timestamp-random.pdf +``` + +**POZNÁMKA:** Timesheet service NEEXISTUJE - všetka logika je priamo v controlleri! + +--- + +## VALIDATORS + +### 1. auth.validators.js +**Účel:** Zod schemas pre autentifikáciu a user management + +**Schemas:** +```javascript +loginSchema + → username: 3-50 chars, required + → password: min 1 char, required + +setPasswordSchema + → newPassword: min 8 chars, obsahuje a-z, A-Z, 0-9, špeciálny znak + → confirmPassword: musí sa zhodovať + → .refine() custom validation pre password match + +linkEmailSchema + → email: valid email format, max 255 chars + → emailPassword: min 1 char + +createUserSchema (admin only) + → username: 3-50 chars, iba [a-zA-Z0-9_-] + → email: optional (ak sa zadá, môže sa linknúť JMAP) + → emailPassword: optional (pre automatické linkovanie) + → firstName, lastName: optional, max 100 chars + +updateUserSchema + → firstName, lastName, email: all optional + +changeRoleSchema + → userId: UUID + → role: enum ['admin', 'member'] +``` + +**Použitie:** +- Všetky `/api/auth/*` routes +- Admin user management routes + +--- + +### 2. crm.validators.js +**Účel:** Zod schemas pre Company, Project, Todo, Note, Time Tracking + +**Company Schemas:** +```javascript +createCompanySchema + → name: required, max 255 chars + → description: optional, max 1000 chars + → address, city, country: optional + → phone: optional, max 50 chars + → email: optional, valid email OR empty string + → website: optional, valid URL OR empty string + +updateCompanySchema + → Všetky fields optional +``` + +**Project Schemas:** +```javascript +createProjectSchema + → name: required, max 255 chars + → companyId: optional UUID OR empty string + → status: enum ['active', 'completed', 'on_hold', 'cancelled'] + → startDate, endDate: optional strings OR empty + +updateProjectSchema + → Všetky fields optional + → NULL support: .or(z.null()) +``` + +**Todo Schemas:** +```javascript +createTodoSchema + → title: required, max 255 chars + → projectId, companyId, assignedTo: optional UUID OR empty + → status: enum ['pending', 'in_progress', 'completed', 'cancelled'] + → priority: enum ['low', 'medium', 'high', 'urgent'] + → dueDate: optional string OR empty + +updateTodoSchema + → Všetky fields optional + NULL support +``` + +**Note Schemas:** +```javascript +createNoteSchema + → content: required, max 5000 chars + → title: optional, max 255 chars + → companyId, projectId, todoId, contactId: optional UUID OR empty + → reminderDate: optional string OR empty + +updateNoteSchema + → Všetky fields optional + NULL support +``` + +**Time Tracking Schemas:** +```javascript +startTimeEntrySchema + → projectId, todoId, companyId: optional UUID (preprocessed to null if empty) + → description: optional, max 1000 chars, trimmed, null if empty + +stopTimeEntrySchema + → Same as start (používa sa pre update pri stop) + +updateTimeEntrySchema + → startTime, endTime: optional ISO strings + → projectId, todoId, companyId, description: optional with preprocessing +``` + +**Helper Functions:** +```javascript +optionalUuid(message) + → Preprocess: undefined, null, '' → null + → Validate: UUID format + → Used for optional foreign keys + +optionalDescription + → Preprocess: trim whitespace, '' → null + → Validate: max 1000 chars + → Nullable +``` + +**Pattern - Empty String Handling:** +```javascript +// Frontend môže poslať empty string namiesto null +.or(z.literal('')) // Accept empty string +.or(z.literal('').or(z.null())) // Update: accept empty OR null +``` + +--- + +### 3. email-account.validators.js +**Účel:** Zod schemas pre email account management + +**Schemas:** +```javascript +createEmailAccountSchema + → email: required, valid format, max 255 chars + → emailPassword: required, min 1 char + +updateEmailAccountSchema + → emailPassword: optional, min 1 char + → isActive: optional boolean + +setPrimaryAccountSchema + → accountId: UUID + → POZNÁMKA: NEVYUŽÍVA SA (endpoint má accountId v path params) +``` + +**Použitie:** +- `/api/email-accounts/*` routes +- JMAP credential validation flow + +--- + ## CONTROLLERS **Účel:** Spracovanie HTTP requestov, volanie services, vracanie responses +### Zoznam Controllerov: +1. **admin.controller.js** - User management (admin only) +2. **auth.controller.js** - Autentifikácia a onboarding +3. **company.controller.js** - Firmy CRUD + nested notes +4. **contact.controller.js** - Email kontakty +5. **crm-email.controller.js** - Email management (read status, search) +6. **email-account.controller.js** - JMAP účty +7. **note.controller.js** - Standalone poznámky (nevyužité) +8. **project.controller.js** - Projekty CRUD + nested notes + team management +9. **todo.controller.js** - Úlohy CRUD +10. **time-tracking.controller.js** - Sledovanie času +11. **timesheet.controller.js** - Upload a download timesheetov (bez service!) + ### Štruktúra každého controllera: ```javascript export const methodName = async (req, res) => { @@ -655,6 +894,19 @@ export const methodName = async (req, res) => { **Účel:** Definícia endpointov, middleware, validácia +### Zoznam Route Files: +1. **admin.routes.js** - User management (Auth + Admin role) +2. **auth.routes.js** - Login, set password, link email (Mixed public/protected) +3. **company.routes.js** - Firmy + nested notes (Auth only) +4. **contact.routes.js** - Kontakty (Auth only) +5. **crm-email.routes.js** - Emaily (Auth only) +6. **email-account.routes.js** - JMAP účty (Auth only) +7. **note.routes.js** - Standalone poznámky (Auth only, nevyužité) +8. **project.routes.js** - Projekty + notes + team (Auth only) +9. **todo.routes.js** - Úlohy (Auth only) +10. **time-tracking.routes.js** - Time tracking (Auth only) +11. **timesheet.routes.js** - Timesheets upload/download (Auth, admin for /all) + ### Štruktúra route file: ```javascript import express from 'express' @@ -697,6 +949,12 @@ export default router ## UTILS +### Zoznam Utility Files: +1. **errors.js** - Custom error classes + formatting +2. **jwt.js** - JWT token generation and validation +3. **logger.js** - Colored console logging +4. **password.js** - Password hashing, encryption, generation + ### 1. errors.js **Účel:** Custom error classy a formatting @@ -1500,14 +1758,40 @@ Order: DESC by startTime ``` Účel: Mesačný prehľad time entries Params: year (YYYY), month (1-12) +Query: userId (optional, admin only – ak je zadaný, načítava sa daný používateľ) Auth: Áno Response: Array of entries pre daný mesiac ``` +#### POST /api/time-tracking/month/:year/:month/generate +``` +Účel: Vygenerovať mesačný timesheet (Excel XLSX) +Params: year (YYYY), month (1-12) +Query: userId (optional, admin only - generate pre iného usera) +Auth: Áno +Body: {} (bez payloadu; posiela sa prázdny objekt) +Response: { + timesheet: { id, fileName, filePath, ... }, + filePath, + entriesCount, + totalMinutes, + totalHours +} +Efekt: + - Vytvorí Excel súbor s time entries pre daný mesiac + - Obsahuje: denné záznamy, projekty, todos, descrip, duration + - Summary: daily totals + overall total + - Uloží do: uploads/timesheets/{userId}/{year}/{month}/ + - INSERT INTO timesheets (isGenerated = true) +Volá: time-tracking.service.generateMonthlyTimesheet() +Admin feature: Admin môže generovať timesheet pre iného usera (query param userId) +``` + #### GET /api/time-tracking/stats/monthly/:year/:month ``` Účel: Mesačné štatistiky Params: year (YYYY), month (1-12) +Query: userId (optional, admin only – ak je zadaný, načítava sa daný používateľ) Auth: Áno Response: { totalMinutes, @@ -1566,39 +1850,70 @@ Validation: #### POST /api/timesheets/upload ``` -Účel: Upload timesheet file +Účel: Upload timesheet file (manuálne nahraný PDF/Excel) Content-Type: multipart/form-data Form: file (PDF/Excel), year (YYYY), month (1-12) Auth: Áno -Validation: Max 10MB +Validation: + - Max 10MB + - Allowed types: PDF, Excel (xlsx, xls) +Efekt: + - Uloží file do: uploads/timesheets/{userId}/{year}/{month}/ + - Generate unique filename: {name}-{timestamp}-{random}.ext + - INSERT INTO timesheets (isGenerated = false) +Response: { timesheet object } ``` -#### GET /api/timesheets/my +#### GET /api/timesheets/my?year=YYYY&month=M ``` -Účel: Moje timesheets +Účel: Moje timesheets (uploaded + generated) +Query: year, month (both optional) Auth: Áno +Response: { timesheets: [...], count } +Order: DESC by uploadedAt ``` -#### GET /api/timesheets/all +#### GET /api/timesheets/all?userId=uuid&year=YYYY&month=M ``` -Účel: Všetky timesheets (admin) +Účel: Všetky timesheets všetkých userov (admin) +Query: userId, year, month (all optional) Auth: Áno (admin only) +Response: { timesheets: [...with user info...], count } +Includes: userId, username, firstName, lastName (LEFT JOIN users) +Order: DESC by uploadedAt ``` #### GET /api/timesheets/:timesheetId/download ``` Účel: Stiahnuť timesheet file Auth: Áno -Response: File download +Permissions: Owner OR admin +Response: File download (res.download) +Errors: + - 404: Timesheet nenájdený alebo súbor neexistuje + - 403: Nemáte oprávnenie (nie vlastník ani admin) ``` #### DELETE /api/timesheets/:timesheetId ``` Účel: Zmazať timesheet Auth: Áno -Efekt: Delete file + DB record +Permissions: Owner OR admin +Efekt: + - Delete file from filesystem (fs.unlink) + - DELETE FROM timesheets + - Continue even if file deletion fails (log error) +Errors: + - 404: Timesheet nenájdený + - 403: Nemáte oprávnenie ``` +**POZNÁMKA:** +- Timesheets môžu byť **uploaded** (manuálne PDF/Excel) alebo **generated** (auto Excel z time entries) +- Field `isGenerated` rozlišuje typ: `true` = auto-generated, `false` = manually uploaded +- Obe typy sa ukladajú do rovnakej tabuľky `timesheets` a rovnakého adresára +- Generated timesheets sa vytvárajú cez `POST /api/time-tracking/month/:year/:month/generate` + --- ## VZŤAHY MEDZI SLUŽBAMI @@ -1978,7 +2293,41 @@ console.log('[DEBUG] JMAP validation:', valid); --- -**Vytvorené:** 2025-11-21 -**Posledná aktualizácia:** 2025-11-24 -**Autor:** CRM Server Team -**Kontakt:** crm-server documentation +**Vytvorené:** 2025-11-21 +**Posledná aktualizácia:** 2025-11-24 +**Autor:** CRM Server Team + +--- + +## CHANGELOG + +### 2025-11-24 - Additions +**Pridané sekcie:** +1. **VALIDATORS** - Kompletná dokumentácia všetkých Zod schemas + - auth.validators.js (login, password, user creation) + - crm.validators.js (company, project, todo, note, time tracking) + - email-account.validators.js (JMAP accounts) + +2. **SERVICES** - Doplnené chýbajúce metódy + - time-tracking.service.generateMonthlyTimesheet() - Excel XLSX generation + +3. **CONTROLLERS** - Pridaný chýbajúci controller + - timesheet.controller.js - File upload/download (bez service layer) + +4. **ROUTES** - Kompletný zoznam všetkých route files + - 11 route files s uvedením middleware requirements + +5. **API ROUTES** - Doplnené chýbajúce endpointy + - POST /api/time-tracking/month/:year/:month/generate - Generate Excel timesheet + - GET /api/timesheets/my - Detail s filters (year, month) + - GET /api/timesheets/all - Admin endpoint s filters + - DELETE /api/timesheets/:timesheetId - Permission checks + +6. **UTILS** - Zoznam všetkých utility files (boli už zdokumentované) + +**Upresnenia:** +- Timesheet service NEEXISTUJE - logika priamo v controlleri +- isGenerated flag rozlišuje uploaded vs generated timesheets +- Admin môže generovať timesheet pre iného usera (query param userId) +- Empty string handling vo validátoroch: `.or(z.literal(''))` pattern +- Optional UUID preprocessing v time tracking schemas diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js index 1c2a2a3..9464e8c 100644 --- a/src/controllers/admin.controller.js +++ b/src/controllers/admin.controller.js @@ -123,10 +123,8 @@ export const getAllUsers = async (req, res) => { res.status(200).json({ success: true, - data: { - users: allUsers, - count: allUsers.length, - }, + count: allUsers.length, + data: allUsers, }); } catch (error) { const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development'); diff --git a/src/controllers/time-tracking.controller.js b/src/controllers/time-tracking.controller.js index a41689c..84750a6 100644 --- a/src/controllers/time-tracking.controller.js +++ b/src/controllers/time-tracking.controller.js @@ -113,10 +113,12 @@ export const getAllTimeEntries = async (req, res) => { export const getMonthlyTimeEntries = async (req, res) => { try { const userId = req.userId; + const userRole = req.user.role; + const targetUserId = userRole === 'admin' && req.query.userId ? req.query.userId : userId; const { year, month } = req.params; const entries = await timeTrackingService.getMonthlyTimeEntries( - userId, + targetUserId, parseInt(year), parseInt(month) ); @@ -139,10 +141,12 @@ export const getMonthlyTimeEntries = async (req, res) => { export const generateMonthlyTimesheet = async (req, res) => { try { const userId = req.userId; + const userRole = req.user.role; + const targetUserId = userRole === 'admin' && req.query.userId ? req.query.userId : userId; const { year, month } = req.params; const result = await timeTrackingService.generateMonthlyTimesheet( - userId, + targetUserId, parseInt(year), parseInt(month) ); @@ -253,10 +257,12 @@ export const deleteTimeEntry = async (req, res) => { export const getMonthlyStats = async (req, res) => { try { const userId = req.userId; + const userRole = req.user.role; + const targetUserId = userRole === 'admin' && req.query.userId ? req.query.userId : userId; const { year, month } = req.params; const stats = await timeTrackingService.getMonthlyStats( - userId, + targetUserId, parseInt(year), parseInt(month) ); diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js index 80ba5c4..9181b09 100644 --- a/src/routes/admin.routes.js +++ b/src/routes/admin.routes.js @@ -9,9 +9,16 @@ import { z } from 'zod'; const router = express.Router(); /** - * Všetky admin routes vyžadujú autentifikáciu a admin rolu + * Routes accessible to all authenticated users */ router.use(authenticate); + +// Zoznam všetkých userov (dostupné pre všetkých autentifikovaných používateľov) +router.get('/users', adminController.getAllUsers); + +/** + * Admin-only routes + */ router.use(requireAdmin); /** @@ -21,9 +28,6 @@ router.use(requireAdmin); // Vytvorenie nového usera router.post('/users', validateBody(createUserSchema), adminController.createUser); -// Zoznam všetkých userov -router.get('/users', adminController.getAllUsers); - // Získanie konkrétneho usera router.get( '/users/:userId', diff --git a/uploads/timesheets/ae65f40f-e22a-45c6-9647-36005c6d31e8/2025/11/timesheet-2025-11-1763975803413.xlsx b/uploads/timesheets/68927352-725c-4e95-adb6-d4002b22bef5/2025/11/timesheet-2025-11-1763979698080.xlsx similarity index 52% rename from uploads/timesheets/ae65f40f-e22a-45c6-9647-36005c6d31e8/2025/11/timesheet-2025-11-1763975803413.xlsx rename to uploads/timesheets/68927352-725c-4e95-adb6-d4002b22bef5/2025/11/timesheet-2025-11-1763979698080.xlsx index 79154cb..b5b9c8f 100644 Binary files a/uploads/timesheets/ae65f40f-e22a-45c6-9647-36005c6d31e8/2025/11/timesheet-2025-11-1763975803413.xlsx and b/uploads/timesheets/68927352-725c-4e95-adb6-d4002b22bef5/2025/11/timesheet-2025-11-1763979698080.xlsx differ