# Личный календарь сотрудников ## Обзор изменений Добавлен третий тип календаря — **Личный календарь сотрудника**. Теперь система поддерживает: 1. **Календарь компании** (company) — глобальные события, доступ только у администраторов 2. **Календарь отдела** (dept) — события отдела, требуется право `MANAGE_CALENDAR` 3. **Личный календарь** (personal) — **НОВОЕ**: индивидуальный календарь каждого сотрудника ## Функциональность ### Личный календарь - ✅ Каждый сотрудник имеет собственный личный календарь - ✅ Полный контроль над своими событиями (создание, редактирование, удаление) - ✅ События личного календаря видны только владельцу - ✅ Другие пользователи не могут изменять чужие личные события - ✅ Администраторы также не могут редактировать личные календари (только просмотр) ## Изменения в Backend ### 1. Модель `CalendarEvent` Добавлено поле `employee`: ```python employee = models.ForeignKey( User, on_delete=models.CASCADE, related_name="personal_calendar_events", verbose_name=_("Сотрудник"), null=True, blank=True, help_text=_("Если задано — личное событие сотрудника."), ) ``` **Логика:** - `department=None, employee=None` → календарь компании - `department=set, employee=None` → календарь отдела - `department=None, employee=set` → личный календарь сотрудника **Валидация:** - Событие не может одновременно принадлежать отделу и сотруднику - Добавлены свойства `is_personal` и обновлён `is_company` ### 2. Сериализаторы **CalendarEventWriteSerializer:** - Добавлено поле `employee_id` в `fields` - Поддержка создания/обновления личных событий ### 3. Views (`CalendarEventsViewSet`) **Новые методы:** ```python def _employee_id(self, *, required: bool = False) -> Optional[int]: """Извлекает employee_id из kwargs/query/body""" ``` **Обновлённые методы:** **`get_queryset()`:** ```python # Приоритет: employee_id > department_id > company if emp is not None: return qs.filter(employee_id=emp, department__isnull=True) elif dep is not None: return qs.filter(department_id=dep, employee__isnull=True) else: return qs.filter(department__isnull=True, employee__isnull=True) ``` **`get_permissions()`:** ```python # Личный календарь — полный контроль только у владельца if emp is not None: if self.request.user.id == emp: return [IsAuthenticated()] else: return [DenyAll()] # Другие пользователи не могут изменять ``` **`perform_create()`:** ```python if emp is not None: serializer.save( employee_id=emp, department_id=None, created_by=self.request.user, ) ``` **`permissions` endpoint:** ```python elif event.employee_id is not None: # Личное событие — редактировать может только владелец if event.employee_id == user.id: can_edit = True can_delete = True ``` ## Изменения в Frontend ### 1. Dropdown выбора календаря **Desktop (`calendar_desktop.html`):** ```html
  • ``` **Mobile (`calendar_mobile.html`):** — аналогично ### 2. JavaScript (`calendarWidget.js`) **State:** ```javascript const state = { type: 'company', // 'company' | 'dept' | 'personal' deptId: null, employeeId: null }; ``` **eventsUrl():** ```javascript const eventsUrl = (deptId = null, employeeId = null) => { const u = new URL(API_EVENTS, location.origin); if (employeeId != null) { u.searchParams.set('employee_id', String(employeeId)); } else if (deptId != null) { u.searchParams.set('department_id', String(deptId)); } return u.pathname + u.search; }; ``` **handleChooserClick():** ```javascript else if (type === 'personal') { state.type = 'personal'; state.deptId = null; const userMeta = document.querySelector('meta[name="user-id"]'); state.employeeId = userMeta ? userMeta.content : null; } ``` **fetchEventsCombined():** ```javascript if (state.type === 'personal' && state.employeeId) { params.employee_id = state.employeeId; } ``` **Form submit:** ```javascript if (state.type === 'personal') { payload.employee_id = Number(state.employeeId); } else if (state.type === 'dept') { payload.department_id = Number(state.deptId); } ``` ### 3. Base Template Добавлен meta-тег с ID пользователя: ```html {% if user.is_authenticated %} {% endif %} ``` ## Миграции Создана миграция `0010_calendarevent_employee_and_more.py`: - Добавляет поле `employee` в модель `CalendarEvent` - Изменяет `help_text` поля `department` ## Тестирование ### Сценарии для проверки: 1. **Переключение на личный календарь:** - Открыть dropdown → выбрать "Личный" - Должны отобразиться только личные события текущего пользователя 2. **Создание личного события:** - В режиме "Личный" создать событие - Событие должно сохраниться с `employee_id=текущий_пользователь` 3. **Редактирование личного события:** - Владелец может редактировать/удалять свои события - ПКМ на событии → контекстное меню должно показывать кнопки 4. **Изоляция календарей:** - События личного календаря не должны видеть другие пользователи - Администраторы могут видеть, но не редактировать чужие личные события 5. **Проверка прав:** - Попытка редактировать чужое личное событие должна вернуть 403 - API endpoint `/permissions/` должен возвращать `can_edit=false` для чужих событий ## API Примеры ### Получить личные события ```http GET /api/v1/calendar/events/?employee_id=123&start=2025-11-01&end=2025-11-30 Authorization: Bearer ``` ### Создать личное событие ```http POST /api/v1/calendar/events/?employee_id=123 Content-Type: application/json Authorization: Bearer { "employee_id": 123, "title": "Личная встреча", "start_date": "2025-11-29", "all_day": true, "recurrence": "one_time" } ``` ### Проверить права на событие ```http GET /api/v1/calendar/events/456/permissions/ Authorization: Bearer Response: { "can_view": true, "can_edit": true, // true только для владельца "can_delete": true } ``` ## Безопасность ✅ **Изоляция данных:** события фильтруются по `employee_id` в QuerySet ✅ **Проверка прав:** `get_permissions()` проверяет владельца перед изменениями ✅ **Валидация:** модель запрещает одновременную привязку к отделу и сотруднику ✅ **API защита:** все endpoints требуют авторизации ## Совместимость ✅ Обратная совместимость сохранена ✅ Существующие события компании/отделов работают без изменений ✅ Миграция безопасна (добавляет nullable поле) ## Файлы изменённые ### Backend: - `backend/calendar_app/models.py` — добавлено поле `employee`, валидация, свойства - `backend/api/v1/calendar/serializers.py` — добавлено поле `employee_id` - `backend/api/v1/calendar/views.py` — логика личных календарей - `backend/calendar_app/migrations/0010_calendarevent_employee_and_more.py` — миграция ### Frontend: - `backend/templates/base.html` — meta-тег `user-id` - `backend/templates/includes/components/calendar_desktop.html` — пункт "Личный" - `backend/templates/includes/components/calendar_mobile.html` — пункт "Личный" - `backend/static/js/components/calendarWidget.js` — полная поддержка личных календарей ## Дальнейшие улучшения - [ ] Возможность "поделиться" личным событием с коллегами - [ ] Синхронизация с внешними календарями (Google, Outlook) - [ ] Уведомления о личных событиях - [ ] Экспорт личного календаря в iCal формат