""" HR app views with comprehensive CRUD operations following healthcare best practices. """ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST, require_GET, require_http_methods from django.template.loader import render_to_string from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView ) from django.urls import reverse_lazy, reverse from django.http import JsonResponse, HttpResponse from django.db.models import Q, Count, Avg, Sum from django.utils import timezone from django.core.paginator import Paginator from django.db import transaction from datetime import datetime, timedelta, date import json from accounts.models import User from appointments.utils import get_tenant_from_request from .models import * from .forms import * from core.utils import AuditLogger class HRDashboardView(LoginRequiredMixin, TemplateView): """ Main HR dashboard with comprehensive statistics and recent activity. """ template_name = 'hr/dashboard.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Basic statistics context.update({ 'total_employees': Employee.objects.filter(tenant=self.request.user.tenant).count(), 'active_employees': Employee.objects.filter( tenant=self.request.user.tenant, employment_status='ACTIVE' ).count(), 'departments': Department.objects.filter(tenant=self.request.user.tenant), 'pending_reviews': PerformanceReview.objects.filter( employee__tenant=self.request.user.tenant, status='PENDING' ).count(), }) # Recent activity context.update({ 'recent_hires': Employee.objects.filter( tenant=self.request.user.tenant, hire_date__gte=timezone.now().date() - timedelta(days=30) ).order_by('-hire_date')[:5], 'recent_reviews': PerformanceReview.objects.filter( employee__tenant=self.request.user.tenant ).order_by('-review_date')[:5], 'recent_training': TrainingRecord.objects.filter( employee__tenant=self.request.user.tenant ).order_by('-completion_date')[:5], }) # Attendance statistics today = timezone.now().date() context.update({ 'employees_clocked_in': TimeEntry.objects.filter( employee__tenant=self.request.user.tenant, clock_in_time__date=today, clock_out_time__isnull=True ).count(), 'total_hours_today': TimeEntry.objects.filter( employee__tenant=self.request.user.tenant, clock_in_time__date=today, clock_out_time__isnull=False ).aggregate( total=Sum('total_hours') )['total'] or 0, }) return context class EmployeeListView(LoginRequiredMixin, ListView): """ List all employees with filtering and search capabilities. """ model = Employee template_name = 'hr/employees/employee_list.html' context_object_name = 'employees' paginate_by = 20 def get_queryset(self): queryset = Employee.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(first_name__icontains=search) | Q(last_name__icontains=search) | Q(employee_id__icontains=search) | Q(email__icontains=search) ) # Filter by department department = self.request.GET.get('department') if department: queryset = queryset.filter(department_id=department) # Filter by employment status status = self.request.GET.get('status') if status: queryset = queryset.filter(employment_status=status) # Filter by position position = self.request.GET.get('position') if position: queryset = queryset.filter(job_title__icontains=position) return queryset.select_related('department').order_by('last_name', 'first_name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['departments'] = Department.objects.filter( tenant=self.request.user.tenant ).order_by('name') context['search'] = self.request.GET.get('search', '') context['selected_department'] = self.request.GET.get('department', '') context['selected_status'] = self.request.GET.get('status', '') context['selected_position'] = self.request.GET.get('position', '') return context class EmployeeDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a specific employee. """ model = Employee template_name = 'hr/employees/employee_detail.html' context_object_name = 'employee' def get_queryset(self): return Employee.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) employee = self.get_object() # Recent time entries context['recent_time_entries'] = TimeEntry.objects.filter( employee=employee ).order_by('-clock_in_time')[:10] # Recent performance reviews context['recent_reviews'] = PerformanceReview.objects.filter( employee=employee ).order_by('-review_date')[:5] # Training records context['training_records'] = TrainingRecord.objects.filter( employee=employee ).order_by('-completion_date')[:10] # Schedule assignments context['current_schedules'] = Schedule.objects.filter( employee=employee, effective_date__lte=timezone.now().date(), end_date__gte=timezone.now().date() if Schedule.end_date else True ) return context class EmployeeCreateView(LoginRequiredMixin, CreateView): """ Create a new employee record. """ model = Employee form_class = EmployeeForm template_name = 'hr/employees/employee_form.html' success_url = reverse_lazy('hr:employee_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant messages.success(self.request, 'Employee created successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class EmployeeUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing employee record. """ model = Employee form_class = EmployeeForm template_name = 'hr/employees/employee_form.html' def get_queryset(self): return Employee.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('hr:employee_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): messages.success(self.request, 'Employee updated successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class EmployeeDeleteView(LoginRequiredMixin, DeleteView): """ Soft delete an employee record (healthcare compliance). """ model = Employee template_name = 'hr/employees/employee_confirm_delete.html' success_url = reverse_lazy('hr:employee_list') def get_queryset(self): return Employee.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): # Soft delete - mark as inactive instead of actual deletion employee = self.get_object() employee.employment_status = 'INACTIVE' employee.termination_date = timezone.now().date() employee.save() messages.success(request, f'Employee {employee.get_full_name()} has been deactivated.') return redirect(self.success_url) class DepartmentListView(LoginRequiredMixin, ListView): """ List all departments with employee counts. """ model = Department template_name = 'hr/departments/department_list.html' context_object_name = 'departments' paginate_by = 20 def get_queryset(self): queryset = Department.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(name__icontains=search) | Q(department_code__icontains=search) | Q(description__icontains=search) ) return queryset class DepartmentDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a specific department. """ model = Department template_name = 'hr/departments/department_detail.html' context_object_name = 'department' def get_queryset(self): return Department.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) department = self.get_object() # Department employees context['employees'] = Employee.objects.filter( department=department ).order_by('last_name', 'first_name') # Department statistics context['active_employees'] = Employee.objects.filter( department=department, employment_status='ACTIVE' ).count() return context class DepartmentCreateView(LoginRequiredMixin, CreateView): """ Create a new department. """ model = Department form_class = DepartmentForm template_name = 'hr/departments/department_form.html' success_url = reverse_lazy('hr:department_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant messages.success(self.request, 'Department created successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class DepartmentUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing department. """ model = Department form_class = DepartmentForm template_name = 'hr/departments/department_form.html' def get_queryset(self): return Department.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('hr:department_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): messages.success(self.request, 'Department updated successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class DepartmentDeleteView(LoginRequiredMixin, DeleteView): """ Delete a department (only if no employees assigned). """ model = Department template_name = 'hr/departments/department_confirm_delete.html' success_url = reverse_lazy('hr:department_list') def get_queryset(self): return Department.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): department = self.get_object() # Check if department has employees if Employee.objects.filter(department=department).exists(): messages.error(request, 'Cannot delete department with assigned employees.') return redirect('hr:department_detail', pk=department.pk) messages.success(request, f'Department {department.name} deleted successfully.') return super().delete(request, *args, **kwargs) class ScheduleListView(LoginRequiredMixin, ListView): """ List all schedules with filtering capabilities. """ model = Schedule template_name = 'hr/schedules/schedule_list.html' context_object_name = 'schedules' paginate_by = 20 def get_queryset(self): queryset = Schedule.objects.filter(employee__tenant=self.request.user.tenant) # Filter by status is_active = self.request.GET.get('is_active') if is_active: queryset = queryset.filter(is_active=(is_active == 'true')) # Filter by date range effective_date = self.request.GET.get('effective_date') end_date = self.request.GET.get('end_date') if effective_date: queryset = queryset.filter(effective_date__gte=effective_date) if end_date: queryset = queryset.filter(end_date__lte=end_date) return queryset.select_related('employee').order_by('-effective_date') class ScheduleDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a specific schedule. """ model = Schedule template_name = 'hr/schedules/schedule_detail.html' context_object_name = 'schedule' def get_queryset(self): return Schedule.objects.filter(employee__tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) schedule = self.get_object() # Schedule assignments context['assignments'] = ScheduleAssignment.objects.filter( schedule=schedule ).order_by('assignment_date', 'start_time') return context class ScheduleCreateView(LoginRequiredMixin, CreateView): """ Create a new schedule. """ model = Schedule form_class = ScheduleForm template_name = 'hr/schedules/schedule_form.html' success_url = reverse_lazy('hr:schedule_list') def form_valid(self, form): form.instance.created_by = self.request.user messages.success(self.request, 'Schedule created successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class ScheduleUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing schedule (limited after publication). """ model = Schedule form_class = ScheduleForm template_name = 'hr/schedules/schedule_form.html' def get_queryset(self): return Schedule.objects.filter(employee__tenant=self.request.user.tenant) def get_success_url(self): return reverse('hr:schedule_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): messages.success(self.request, 'Schedule updated successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class ScheduleAssignmentListView(LoginRequiredMixin, ListView): """ List all schedule assignments with filtering capabilities. """ model = ScheduleAssignment template_name = 'hr/assignments/schedule_assignment_list.html' context_object_name = 'assignments' paginate_by = 20 def get_queryset(self): queryset = ScheduleAssignment.objects.filter( schedule__employee__tenant=self.request.user.tenant ) # Filter by employee employee = self.request.GET.get('employee') if employee: queryset = queryset.filter(schedule__employee_id=employee) # Filter by date range start_date = self.request.GET.get('start_date') end_date = self.request.GET.get('end_date') if start_date: queryset = queryset.filter(assignment_date__gte=start_date) if end_date: queryset = queryset.filter(assignment_date__lte=end_date) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) return queryset.select_related('schedule__employee', 'department').order_by('assignment_date', 'start_time') class ScheduleAssignmentCreateView(LoginRequiredMixin, CreateView): """ Create a new schedule assignment. """ model = ScheduleAssignment form_class = ScheduleAssignmentForm template_name = 'hr/assignments/schedule_assignment_form.html' success_url = reverse_lazy('hr:schedule_assignment_list') def form_valid(self, form): messages.success(self.request, 'Schedule assignment created successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class ScheduleAssignmentUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing schedule assignment. """ model = ScheduleAssignment form_class = ScheduleAssignmentForm template_name = 'hr/assignments/schedule_assignment_form.html' success_url = reverse_lazy('hr:schedule_assignment_list') def get_queryset(self): return ScheduleAssignment.objects.filter(schedule__employee__tenant=self.request.user.tenant) def form_valid(self, form): messages.success(self.request, 'Schedule assignment updated successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class ScheduleAssignmentDeleteView(LoginRequiredMixin, DeleteView): model = ScheduleAssignment template_name = 'hr/assignments/schedule_assignment_confirm_delete.html' success_url = reverse_lazy('hr:schedule_assignment_list') def get_queryset(self): queryset = ScheduleAssignment.objects.filter(schedule__employee__tenant=self.request.user.tenant) employee = self.request.GET.get('employee') if employee: queryset = queryset.filter(schedule__employee_id=employee) class TimeEntryListView(LoginRequiredMixin, ListView): """ List time entries with filtering capabilities. """ model = TimeEntry template_name = 'hr/time_entries/time_entry_list.html' context_object_name = 'time_entries' paginate_by = 20 def get_queryset(self): queryset = TimeEntry.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee') # Filter by employee employee = self.request.GET.get('employee') if employee: queryset = queryset.filter(employee_id=employee) # Filter by date range start_date = self.request.GET.get('start_date') end_date = self.request.GET.get('end_date') if start_date: queryset = queryset.filter(work_date__gte=start_date) if end_date: queryset = queryset.filter(work_date__lte=end_date) # Filter by approval status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) return queryset.order_by('-work_date') class TimeEntryDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a specific time entry. """ model = TimeEntry template_name = 'hr/time_entries/time_entry_detail.html' context_object_name = 'time_entry' def get_queryset(self): return TimeEntry.objects.filter(employee__tenant=self.request.user.tenant) class TimeEntryCreateView(LoginRequiredMixin, CreateView): """ Create a new time entry (manual entry). """ model = TimeEntry form_class = TimeEntryForm template_name = 'hr/time_entries/time_entry_form.html' success_url = reverse_lazy('hr:time_entry_list') def form_valid(self, form): messages.success(self.request, 'Time entry created successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class TimeEntryUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing time entry (limited fields). """ model = TimeEntry form_class = TimeEntryForm template_name = 'hr/time_entries/time_entry_form.html' success_url = reverse_lazy('hr:time_entry_list') def get_queryset(self): return TimeEntry.objects.filter(employee__tenant=self.request.user.tenant) def form_valid(self, form): # Restrict updates for approved entries if self.object.status == 'APPROVED' and not self.request.user.is_superuser: messages.error(self.request, 'Cannot modify approved time entries.') return redirect('hr:time_entry_detail', pk=self.object.pk) messages.success(self.request, 'Time entry updated successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class PerformanceReviewListView(LoginRequiredMixin, ListView): """ List performance reviews with filtering capabilities. """ model = PerformanceReview template_name = 'hr/reviews/performance_review_list.html' context_object_name = 'reviews' paginate_by = 20 def get_queryset(self): queryset = PerformanceReview.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee', 'reviewer') # Filter by employee employee = self.request.GET.get('employee') if employee: queryset = queryset.filter(employee_id=employee) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by review type review_type = self.request.GET.get('review_type') if review_type: queryset = queryset.filter(review_type=review_type) return queryset.order_by('-review_date') class PerformanceReviewDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a specific performance review. """ model = PerformanceReview template_name = 'hr/reviews/performance_review_detail.html' context_object_name = 'review' def get_queryset(self): # Eager load employee + department + supervisor to reduce queries return (PerformanceReview.objects .select_related('employee', 'employee__department', 'employee__supervisor', 'reviewer') .filter(employee__tenant=self.request.user.tenant)) def get_object(self, queryset=None): queryset = queryset or self.get_queryset() return get_object_or_404(queryset, pk=self.kwargs.get('pk') or self.kwargs.get('id')) @staticmethod def _split_lines(text): if not text: return [] parts = [] for line in str(text).replace('\r', '').split('\n'): for piece in line.split(';'): piece = piece.strip() if piece: parts.append(piece) return parts def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) review = ctx['review'] # Build categories from competency_ratings JSON: { "Teamwork": 4.0, ... } # Make a list of dicts the template can iterate through. categories = [] ratings = review.competency_ratings or {} # Keep a stable order (by key) to avoid chart jitter for name in sorted(ratings.keys()): try: score = float(ratings[name]) except (TypeError, ValueError): score = 0.0 categories.append({'name': name, 'score': score, 'comments': ''}) # Previous reviews (same employee, exclude current) previous_reviews = ( PerformanceReview.objects .filter(employee=review.employee) .exclude(pk=review.pk) .order_by('-review_date')[:5] .select_related('employee') ) # Strengths / AFI lists for bullet rendering strengths_list = self._split_lines(review.strengths) afi_list = self._split_lines(review.areas_for_improvement) # Goals blocks as lists for nicer display goals_achieved_list = self._split_lines(review.goals_achieved) goals_not_achieved_list = self._split_lines(review.goals_not_achieved) future_goals_list = self._split_lines(review.future_goals) ctx.update({ 'categories': categories, 'previous_reviews': previous_reviews, 'review_strengths_list': strengths_list, 'review_afi_list': afi_list, 'goals_achieved_list': goals_achieved_list, 'goals_not_achieved_list': goals_not_achieved_list, 'future_goals_list': future_goals_list, }) # convenience on the review object (template already expects these list props sometimes) setattr(review, 'strengths_list', strengths_list) setattr(review, 'areas_for_improvement_list', afi_list) return ctx class PerformanceReviewCreateView(LoginRequiredMixin, CreateView): """ Create a new performance review. """ model = PerformanceReview form_class = PerformanceReviewForm template_name = 'hr/reviews/performance_review_form.html' success_url = reverse_lazy('hr:performance_review_list') def form_valid(self, form): form.instance.reviewer = self.request.user messages.success(self.request, 'Performance review created successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class PerformanceReviewUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing performance review. """ model = PerformanceReview form_class = PerformanceReviewForm template_name = 'hr/reviews/performance_review_form.html' def get_queryset(self): return PerformanceReview.objects.filter(employee__tenant=self.request.user.tenant) def get_success_url(self): return reverse('hr:performance_review_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): messages.success(self.request, 'Performance review updated successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class PerformanceReviewDeleteView(LoginRequiredMixin, DeleteView): """ Delete a performance review (only if not finalized). """ model = PerformanceReview template_name = 'hr/reviews/performance_review_confirm_delete.html' context_object_name = 'review' success_url = reverse_lazy('hr:performance_review_list') def get_queryset(self): return PerformanceReview.objects.filter(employee__tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): review = self.get_object() # Check if review is finalized if review.status == 'COMPLETED': messages.error(request, 'Cannot delete completed performance reviews.') return redirect('hr:performance_review_detail', pk=review.pk) messages.success(request, 'Performance review deleted successfully.') return super().delete(request, *args, **kwargs) class TrainingManagementView(LoginRequiredMixin, ListView): model = TrainingRecord template_name = 'hr/training/training_management.html' context_object_name = 'training_records' paginate_by = 20 def get_queryset(self): tenant = self.request.user.tenant qs = (TrainingRecord.objects .filter(employee__tenant=tenant) .select_related('employee', 'employee__department') .order_by( '-completion_date')) # optional GET filters (works with the template’s inputs) if emp := self.request.GET.get('employee'): qs = qs.filter(employee_id=emp) if ttype := self.request.GET.get('training_type'): qs = qs.filter(training_type=ttype) if status := self.request.GET.get('status'): qs = qs.filter(status=status) return qs def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) tenant = self.request.user.tenant today = timezone.now().date() base = TrainingRecord.objects.filter(employee__tenant=tenant) total_records = base.count() completed_trainings = base.filter(status='COMPLETED').count() pending_trainings = base.filter(status__in=['SCHEDULED', 'IN_PROGRESS']).count() overdue_trainings = base.filter(expiry_date__lt=today, expiry_date__isnull=False).exclude( status='COMPLETED').count() # compliance rate = “not expired” among trainings that have an expiry_date with_expiry = base.filter(expiry_date__isnull=False) valid_now = with_expiry.filter(expiry_date__gte=today).count() compliance_rate = round((valid_now / with_expiry.count()) * 100, 1) if with_expiry.exists() else 100.0 # expiring soon = within 30 days expiring_soon_count = with_expiry.filter(expiry_date__gte=today, expiry_date__lte=today + timedelta(days=30)).count() # department compliance (simple example: percent of non-expired per dept among those with expiry) dept_rows = [] departments = Department.objects.filter(tenant=tenant).order_by('name') for d in departments: d_qs = with_expiry.filter(employee__department=d) if not d_qs.exists(): rate = 100 else: ok = d_qs.filter(expiry_date__gte=today).count() rate = round((ok / d_qs.count()) * 100) color = 'success' if rate >= 90 else 'warning' if rate >= 70 else 'danger' dept_rows.append({'name': d.name, 'compliance_rate': rate, 'compliance_color': color}) # “compliance alerts” demo (overdue/expiring soon) alerts = [] for tr in base.select_related('employee'): if tr.expiry_date: if tr.expiry_date < today: alerts.append({ 'id': tr.id, 'employee': tr.employee, 'requirement': tr.training_name, 'due_date': tr.expiry_date, 'priority_color': 'danger', 'urgency_color': 'danger', 'get_priority_display': 'Overdue', }) elif today <= tr.expiry_date <= today + timedelta(days=30): alerts.append({ 'id': tr.id, 'employee': tr.employee, 'requirement': tr.training_name, 'due_date': tr.expiry_date, 'priority_color': 'warning', 'urgency_color': 'warning', 'get_priority_display': 'Expiring Soon', }) ctx.update({ 'total_records': total_records, 'completed_trainings': completed_trainings, 'pending_trainings': pending_trainings, 'overdue_trainings': overdue_trainings, 'departments': departments, 'compliance_rate': compliance_rate, 'expiring_soon_count': expiring_soon_count, 'department_compliance': dept_rows, 'compliance_alerts': alerts, }) return ctx class TrainingRecordListView(LoginRequiredMixin, ListView): """ List training records with filtering capabilities. """ model = TrainingRecord template_name = 'hr/training/training_record_list.html' context_object_name = 'training_records' paginate_by = 20 def get_queryset(self): queryset = TrainingRecord.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee') # Filter by employee employee = self.request.GET.get('employee') if employee: queryset = queryset.filter(employee_id=employee) # Filter by training type training_type = self.request.GET.get('training_type') if training_type: queryset = queryset.filter(training_type=training_type) # Filter by completion status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) return queryset.order_by('-completion_date') class TrainingRecordDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a specific training record. """ model = TrainingRecord template_name = 'hr/training/training_record_detail.html' context_object_name = 'record' def get_queryset(self): tenant = self.request.user.tenant return TrainingRecord.objects.filter(employee__tenant=tenant) class TrainingRecordCreateView(LoginRequiredMixin, CreateView): """ Create a new training record. """ model = TrainingRecord form_class = TrainingRecordForm template_name = 'hr/training/training_record_form.html' success_url = reverse_lazy('hr:training_record_list') def form_valid(self, form): form.instance.created_by = self.request.user messages.success(self.request, 'Training record created successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class TrainingRecordUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing training record. """ model = TrainingRecord form_class = TrainingRecordForm template_name = 'hr/training/training_record_form.html' def get_queryset(self): return TrainingRecord.objects.filter(employee__tenant=self.request.user.tenant) def get_success_url(self): return reverse('hr:training_record_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): messages.success(self.request, 'Training record updated successfully.') return super().form_valid(form) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs class TrainingRecordDeleteView(LoginRequiredMixin, DeleteView): """ Delete a training record. """ model = TrainingRecord template_name = 'hr/training/training_record_confirm_delete.html' success_url = reverse_lazy('hr:training_record_list') context_object_name = 'record' def get_queryset(self): return TrainingRecord.objects.filter(employee__tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): messages.success(request, 'Training record deleted successfully.') return super().delete(request, *args, **kwargs) class TrainingProgramListView(LoginRequiredMixin, ListView): """List all training programs.""" model = TrainingPrograms template_name = 'hr/training/program_list.html' context_object_name = 'programs' paginate_by = 20 def get_queryset(self): queryset = TrainingPrograms.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(name__icontains=search) | Q(description__icontains=search) | Q(program_provider__icontains=search) ) # Filter by program type program_type = self.request.GET.get('program_type') if program_type: queryset = queryset.filter(program_type=program_type) # Filter by certification status is_certified = self.request.GET.get('is_certified') if is_certified: queryset = queryset.filter(is_certified=(is_certified == 'true')) return queryset.select_related('instructor').order_by('name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['program_types'] = TrainingPrograms.TrainingType.choices context['search'] = self.request.GET.get('search', '') context['selected_type'] = self.request.GET.get('program_type', '') context['selected_certified'] = self.request.GET.get('is_certified', '') return context class TrainingProgramDetailView(LoginRequiredMixin, DetailView): """Display detailed information about a training program.""" model = TrainingPrograms template_name = 'hr/training/program_detail.html' context_object_name = 'program' def get_queryset(self): return TrainingPrograms.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) program = self.get_object() # Get program modules context['modules'] = ProgramModule.objects.filter(program=program).order_by('order') # Get prerequisites context['prerequisites'] = ProgramPrerequisite.objects.filter( program=program ).select_related('required_program') # Get upcoming sessions context['upcoming_sessions'] = TrainingSession.objects.filter( program=program, start_at__gte=timezone.now() ).order_by('start_at')[:5] # Get recent completions context['recent_completions'] = TrainingRecord.objects.filter( program=program, status='COMPLETED' ).select_related('employee').order_by('-completion_date')[:10] # Statistics context['total_enrollments'] = TrainingRecord.objects.filter(program=program).count() context['completion_rate'] = 0 if context['total_enrollments'] > 0: completed = TrainingRecord.objects.filter(program=program, status='COMPLETED').count() context['completion_rate'] = round((completed / context['total_enrollments']) * 100, 1) return context class TrainingProgramCreateView(LoginRequiredMixin, CreateView): """Create a new training program.""" model = TrainingPrograms form_class = TrainingProgramForm template_name = 'hr/training/program_form.html' def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.created_by = self.request.user.employee_profile messages.success(self.request, 'Training program created successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:training_program_detail', kwargs={'pk': self.object.pk}) class TrainingProgramUpdateView(LoginRequiredMixin, UpdateView): """Update an existing training program.""" model = TrainingPrograms template_name = 'hr/training/program_form.html' form_class = TrainingProgramForm def get_queryset(self): return TrainingPrograms.objects.filter(tenant=self.request.user.tenant) def form_valid(self, form): messages.success(self.request, 'Training program updated successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:training_program_detail', kwargs={'pk': self.object.pk}) class TrainingSessionListView(LoginRequiredMixin, ListView): """List all training sessions.""" model = TrainingSession template_name = 'hr/training/session_list.html' context_object_name = 'sessions' paginate_by = 20 def get_queryset(self): queryset = TrainingSession.objects.filter( program__tenant=self.request.user.tenant ).select_related('program', 'instructor') # Filter by program program_id = self.request.GET.get('program') if program_id: queryset = queryset.filter(program_id=program_id) # Filter by delivery method delivery_method = self.request.GET.get('delivery_method') if delivery_method: queryset = queryset.filter(delivery_method=delivery_method) # Filter by date range start_date = self.request.GET.get('start_date') if start_date: queryset = queryset.filter(start_at__date__gte=start_date) end_date = self.request.GET.get('end_date') if end_date: queryset = queryset.filter(start_at__date__lte=end_date) return queryset.order_by('start_at') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['programs'] = TrainingPrograms.objects.filter( tenant=self.request.user.tenant ).order_by('name') context['delivery_methods'] = TrainingSession.TrainingDelivery.choices return context class TrainingSessionDetailView(LoginRequiredMixin, DetailView): """Display detailed information about a training session.""" model = TrainingSession template_name = 'hr/training/session_detail.html' context_object_name = 'session' def get_queryset(self): return TrainingSession.objects.filter( program__tenant=self.request.user.tenant ).select_related('program', 'instructor') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) session = self.get_object() # Get enrollments context['enrollments'] = TrainingRecord.objects.filter( session=session ).select_related('employee').order_by('enrolled_at') # Get attendance records context['attendance_records'] = TrainingAttendance.objects.filter( enrollment__session=session ).select_related('enrollment__employee') # Statistics context['total_enrolled'] = context['enrollments'].count() context['capacity_percentage'] = 0 if session.capacity > 0: context['capacity_percentage'] = round( (context['total_enrolled'] / session.capacity) * 100, 1 ) return context class TrainingSessionCreateView(LoginRequiredMixin, CreateView): """Create a new training session.""" model = TrainingSession form_class = TrainingSessionForm template_name = 'hr/training/session_form.html' def form_valid(self, form): form.instance.created_by = self.request.user.employee_profile messages.success(self.request, 'Training session created successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:training_session_detail', kwargs={'pk': self.object.pk}) class TrainingSessionUpdateView(LoginRequiredMixin, UpdateView): """Update an existing training session.""" model = TrainingSession form_class = TrainingSessionForm template_name = 'hr/training/session_form.html' def get_queryset(self): return TrainingSession.objects.filter(program__tenant=self.request.user.tenant) def form_valid(self, form): messages.success(self.request, 'Training session updated successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:training_session_detail', kwargs={'pk': self.object.pk}) class TrainingCertificateListView(LoginRequiredMixin, ListView): """List all training certificates.""" model = TrainingCertificates template_name = 'hr/training/certificate_list.html' context_object_name = 'certificates' paginate_by = 20 def get_queryset(self): queryset = TrainingCertificates.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee', 'program', 'enrollment') # Filter by employee employee_id = self.request.GET.get('employee') if employee_id: queryset = queryset.filter(employee_id=employee_id) # Filter by program program_id = self.request.GET.get('program') if program_id: queryset = queryset.filter(program_id=program_id) # Filter by expiry status expiry_status = self.request.GET.get('expiry_status') today = timezone.now().date() if expiry_status == 'valid': queryset = queryset.filter( Q(expiry_date__isnull=True) | Q(expiry_date__gt=today) ) elif expiry_status == 'expiring': queryset = queryset.filter( expiry_date__gt=today, expiry_date__lte=today + timedelta(days=30) ) elif expiry_status == 'expired': queryset = queryset.filter(expiry_date__lt=today) return queryset.order_by('-issued_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['employees'] = Employee.objects.filter( tenant=self.request.user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') context['programs'] = TrainingPrograms.objects.filter( tenant=self.request.user.tenant, is_certified=True ).order_by('name') return context class TrainingCertificateDetailView(LoginRequiredMixin, DetailView): """Display detailed information about a training certificate.""" model = TrainingCertificates template_name = 'hr/training/certificate_detail.html' context_object_name = 'certificate' def get_queryset(self): return TrainingCertificates.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee', 'program', 'enrollment', 'signed_by') class LeaveRequestListView(LoginRequiredMixin, ListView): """ List all leave requests with filtering capabilities. """ model = LeaveRequest template_name = 'hr/leave/leave_request_list.html' context_object_name = 'leave_requests' paginate_by = 20 def get_queryset(self): queryset = LeaveRequest.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee', 'leave_type', 'current_approver') # Filter by employee employee_id = self.request.GET.get('employee') if employee_id: queryset = queryset.filter(employee_id=employee_id) # Filter by leave type leave_type_id = self.request.GET.get('leave_type') if leave_type_id: queryset = queryset.filter(leave_type_id=leave_type_id) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by date range start_date = self.request.GET.get('start_date') if start_date: queryset = queryset.filter(start_date__gte=start_date) end_date = self.request.GET.get('end_date') if end_date: queryset = queryset.filter(end_date__lte=end_date) # Search search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(employee__first_name__icontains=search) | Q(employee__last_name__icontains=search) | Q(reason__icontains=search) ) return queryset.order_by('-created_at') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['employees'] = Employee.objects.filter( tenant=self.request.user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') context['leave_types'] = LeaveType.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name') context['statuses'] = LeaveRequest.RequestStatus.choices return context class LeaveRequestDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a leave request. """ model = LeaveRequest template_name = 'hr/leave/leave_request_detail.html' context_object_name = 'leave_request' def get_queryset(self): return LeaveRequest.objects.filter( employee__tenant=self.request.user.tenant ).select_related( 'employee', 'leave_type', 'current_approver', 'final_approver', 'cancelled_by' ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) leave_request = self.get_object() # Get approval history context['approvals'] = LeaveApproval.objects.filter( leave_request=leave_request ).select_related('approver', 'delegated_by').order_by('level') # Get employee's leave balance try: context['balance'] = LeaveBalance.objects.get( employee=leave_request.employee, leave_type=leave_request.leave_type, year=leave_request.start_date.year ) except LeaveBalance.DoesNotExist: context['balance'] = None # Check if current user can approve if hasattr(self.request.user, 'employee_profile'): context['can_approve'] = ( leave_request.current_approver == self.request.user.employee_profile and leave_request.status == 'PENDING' ) else: context['can_approve'] = False return context class LeaveRequestCreateView(LoginRequiredMixin, CreateView): """ Create a new leave request. """ model = LeaveRequest form_class = LeaveRequestForm template_name = 'hr/leave/leave_request_form.html' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user if hasattr(self.request.user, 'employee_profile'): kwargs['employee'] = self.request.user.employee_profile return kwargs def form_valid(self, form): if hasattr(self.request.user, 'employee_profile'): form.instance.employee = self.request.user.employee_profile form.instance.created_by = self.request.user form.instance.status = 'PENDING' form.instance.submitted_at = timezone.now() messages.success(self.request, 'Leave request submitted successfully.') return super().form_valid(form) else: messages.error(self.request, 'You must have an employee profile to request leave.') return redirect('hr:leave_request_list') def get_success_url(self): return reverse('hr:leave_request_detail', kwargs={'pk': self.object.pk}) class LeaveRequestUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing leave request (only if in DRAFT status). """ model = LeaveRequest form_class = LeaveRequestForm template_name = 'hr/leave/leave_request_form.html' def get_queryset(self): return LeaveRequest.objects.filter( employee__tenant=self.request.user.tenant, status='DRAFT' ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user if hasattr(self.request.user, 'employee_profile'): kwargs['employee'] = self.request.user.employee_profile return kwargs def form_valid(self, form): messages.success(self.request, 'Leave request updated successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:leave_request_detail', kwargs={'pk': self.object.pk}) class SalaryListView(LoginRequiredMixin, ListView): """ List all salary records with filtering capabilities. """ model = SalaryInformation template_name = 'hr/salary/salary_list.html' context_object_name = 'salary_records' paginate_by = 20 def get_queryset(self): queryset = SalaryInformation.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee', 'employee__department') # Filter by employee employee_id = self.request.GET.get('employee') if employee_id: queryset = queryset.filter(employee_id=employee_id) # Filter by currency currency = self.request.GET.get('currency') if currency: queryset = queryset.filter(currency=currency) # Filter by payment frequency payment_frequency = self.request.GET.get('payment_frequency') if payment_frequency: queryset = queryset.filter(payment_frequency=payment_frequency) # Filter by active status is_active = self.request.GET.get('is_active') if is_active: queryset = queryset.filter(is_active=(is_active == 'true')) # Search search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(employee__first_name__icontains=search) | Q(employee__last_name__icontains=search) | Q(employee__employee_id__icontains=search) ) return queryset.order_by('-effective_date', 'employee__last_name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['employees'] = Employee.objects.filter( tenant=self.request.user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') context['currencies'] = SalaryInformation.Currency.choices context['payment_frequencies'] = SalaryInformation.PaymentFrequency.choices return context class SalaryCreateView(LoginRequiredMixin, CreateView): """ Create a new salary record. """ model = SalaryInformation form_class = SalaryInformationForm template_name = 'hr/salary/salary_form.html' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.created_by = self.request.user messages.success(self.request, 'Salary record created successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:salary_detail', kwargs={'pk': self.object.pk}) class SalaryDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a salary record. """ model = SalaryInformation template_name = 'hr/salary/salary_detail.html' context_object_name = 'salary' def get_queryset(self): return SalaryInformation.objects.filter( employee__tenant=self.request.user.tenant ).select_related('employee', 'employee__department', 'created_by') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) salary = self.get_object() # Get salary adjustment history context['adjustments'] = SalaryAdjustment.objects.filter( Q(previous_salary=salary) | Q(new_salary=salary) ).select_related('employee', 'approved_by').order_by('-created_at') # Get employee's salary history context['salary_history'] = SalaryInformation.objects.filter( employee=salary.employee, employee__tenant=self.request.user.tenant ).order_by('-effective_date')[:10] return context class SalaryUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing salary record. """ model = SalaryInformation form_class = SalaryInformationForm template_name = 'hr/salary/salary_form.html' def get_queryset(self): return SalaryInformation.objects.filter(employee__tenant=self.request.user.tenant) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): messages.success(self.request, 'Salary record updated successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:salary_detail', kwargs={'pk': self.object.pk}) class SalaryDeleteView(LoginRequiredMixin, DeleteView): """ Delete a salary record (soft delete by marking as inactive). """ model = SalaryInformation template_name = 'hr/salary/salary_confirm_delete.html' success_url = reverse_lazy('hr:salary_list') def get_queryset(self): return SalaryInformation.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): salary = self.get_object() # Soft delete - mark as inactive salary.is_active = False salary.end_date = timezone.now().date() salary.save() messages.success(request, f'Salary record for {salary.employee.get_full_name()} has been deactivated.') return redirect(self.success_url) class LeaveBalanceListView(LoginRequiredMixin, ListView): """ List leave balances for all employees. """ model = LeaveBalance template_name = 'hr/leave/leave_balance_list.html' context_object_name = 'balances' paginate_by = 20 def get_queryset(self): current_year = date.today().year queryset = LeaveBalance.objects.filter( employee__tenant=self.request.user.tenant, year=current_year ).select_related('employee', 'leave_type') # Filter by employee employee_id = self.request.GET.get('employee') if employee_id: queryset = queryset.filter(employee_id=employee_id) # Filter by leave type leave_type_id = self.request.GET.get('leave_type') if leave_type_id: queryset = queryset.filter(leave_type_id=leave_type_id) return queryset.order_by('employee__last_name', 'employee__first_name', 'leave_type__name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['employees'] = Employee.objects.filter( tenant=self.request.user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') context['leave_types'] = LeaveType.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name') context['current_year'] = date.today().year return context class LeaveDelegateListView(LoginRequiredMixin, ListView): """ List all leave delegations. """ model = LeaveDelegate template_name = 'hr/leave/leave_delegate_list.html' context_object_name = 'delegations' paginate_by = 20 def get_queryset(self): queryset = LeaveDelegate.objects.filter( delegator__tenant=self.request.user.tenant ).select_related('delegator', 'delegate') # Filter by active status is_active = self.request.GET.get('is_active') if is_active: queryset = queryset.filter(is_active=(is_active == 'true')) # Filter by current status show_current = self.request.GET.get('show_current') if show_current == 'true': today = date.today() queryset = queryset.filter( is_active=True, start_date__lte=today, end_date__gte=today ) return queryset.order_by('-start_date') class LeaveDelegateCreateView(LoginRequiredMixin, CreateView): """ Create a new leave delegation. """ model = LeaveDelegate form_class = LeaveDelegateForm template_name = 'hr/leave/leave_delegate_form.html' success_url = reverse_lazy('hr:leave_delegate_list') def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user if hasattr(self.request.user, 'employee_profile'): kwargs['delegator'] = self.request.user.employee_profile return kwargs def form_valid(self, form): if hasattr(self.request.user, 'employee_profile'): form.instance.delegator = self.request.user.employee_profile form.instance.created_by = self.request.user messages.success(self.request, 'Leave delegation created successfully.') return super().form_valid(form) else: messages.error(request, 'You must have an employee profile to create delegations.') return redirect('hr:leave_delegate_list') class SalaryAdjustmentCreateView(LoginRequiredMixin, CreateView): """ Create a new salary adjustment. """ model = SalaryAdjustment form_class = SalaryAdjustmentForm template_name = 'hr/salary/salary_adjustment_form.html' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.created_by = self.request.user # Auto-approve if user has permission if self.request.user.has_perm('hr.approve_salaryadjustment'): form.instance.approved_by = self.request.user form.instance.approval_date = timezone.now() messages.success(self.request, 'Salary adjustment created successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:salary_adjustment_list') class SalaryAdjustmentListView(LoginRequiredMixin, ListView): """ List all salary adjustments with filtering capabilities. """ model = SalaryAdjustment template_name = 'hr/salary/salary_adjustment_list.html' context_object_name = 'adjustments' paginate_by = 20 def get_queryset(self): queryset = SalaryAdjustment.objects.filter( tenant=self.request.user.tenant ).select_related('employee', 'previous_salary', 'new_salary', 'approved_by') # Filter by employee employee_id = self.request.GET.get('employee') if employee_id: queryset = queryset.filter(employee_id=employee_id) # Filter by adjustment type adjustment_type = self.request.GET.get('adjustment_type') if adjustment_type: queryset = queryset.filter(adjustment_type=adjustment_type) # Filter by approval status is_approved = self.request.GET.get('is_approved') if is_approved == 'true': queryset = queryset.filter(approved_by__isnull=False) elif is_approved == 'false': queryset = queryset.filter(approved_by__isnull=True) # Filter by date range start_date = self.request.GET.get('start_date') if start_date: queryset = queryset.filter(adjustment_date__gte=start_date) end_date = self.request.GET.get('end_date') if end_date: queryset = queryset.filter(adjustment_date__lte=end_date) return queryset.order_by('-adjustment_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['employees'] = Employee.objects.filter( tenant=self.request.user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') context['adjustment_types'] = SalaryAdjustment.AdjustmentType.choices return context class DocumentRequestListView(LoginRequiredMixin, ListView): """ List all document requests with filtering capabilities. """ model = DocumentRequest template_name = 'hr/documents/document_request_list.html' context_object_name = 'document_requests' paginate_by = 20 def get_queryset(self): tenant = self.request.user.tenant queryset = DocumentRequest.objects.filter( employee__tenant=tenant ).select_related('employee', 'employee__department', 'processed_by') # Filter by employee employee_id = self.request.GET.get('employee') if employee_id: queryset = queryset.filter(employee_id=employee_id) # Filter by document type document_type = self.request.GET.get('document_type') if document_type: queryset = queryset.filter(document_type=document_type) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by language language = self.request.GET.get('language') if language: queryset = queryset.filter(language=language) # Filter by delivery method delivery_method = self.request.GET.get('delivery_method') if delivery_method: queryset = queryset.filter(delivery_method=delivery_method) # Filter by urgent is_urgent = self.request.GET.get('is_urgent') if is_urgent == 'true': queryset = queryset.filter( required_by_date__lte=timezone.now().date() + timedelta(days=3) ) # Search search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(employee__first_name__icontains=search) | Q(employee__last_name__icontains=search) | Q(document_number__icontains=search) | Q(purpose__icontains=search) ) return queryset.order_by('-created_at') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['employees'] = Employee.objects.filter( tenant=self.request.user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') context['document_types'] = DocumentRequest.DocumentType.choices context['statuses'] = DocumentRequest.RequestStatus.choices context['languages'] = DocumentRequest.Language.choices context['delivery_methods'] = DocumentRequest.DeliveryMethod.choices return context class DocumentRequestCreateView(LoginRequiredMixin, CreateView): """ Create a new document request. """ model = DocumentRequest form_class = DocumentRequestForm template_name = 'hr/documents/document_request_form.html' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user if hasattr(self.request.user, 'employee_profile'): kwargs['employee'] = self.request.user.employee_profile return kwargs def form_valid(self, form): form.instance.tenant = self.request.user.tenant # Set employee if not already set if not form.instance.employee and hasattr(self.request.user, 'employee_profile'): form.instance.employee = self.request.user.employee_profile form.instance.created_by = self.request.user form.instance.status = 'PENDING' messages.success(self.request, 'Document request submitted successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:document_request_detail', kwargs={'pk': self.object.pk}) class DocumentRequestDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a document request. """ model = DocumentRequest template_name = 'hr/documents/document_request_detail.html' context_object_name = 'document_request' def get_queryset(self): tenant = self.request.user.tenant return DocumentRequest.objects.filter( employee__tenant=tenant ).select_related('employee', 'employee__department', 'processed_by', 'created_by') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) document_request = self.get_object() # Check if current user can process context['can_process'] = ( self.request.user.has_perm('hr.change_documentrequest') and document_request.status in ['PENDING', 'IN_PROGRESS'] ) # Check if current user can update (only if draft or pending and is owner) context['can_update'] = ( hasattr(self.request.user, 'employee_profile') and document_request.employee == self.request.user.employee_profile and document_request.status in ['DRAFT', 'PENDING'] ) # Check if current user can cancel context['can_cancel'] = ( hasattr(self.request.user, 'employee_profile') and document_request.employee == self.request.user.employee_profile and document_request.status in ['PENDING', 'IN_PROGRESS'] ) return context class DocumentRequestUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing document request (only if in DRAFT or PENDING status). """ model = DocumentRequest form_class = DocumentRequestForm template_name = 'hr/documents/document_request_form.html' def get_queryset(self): tenant = self.request.user.tenant # Only allow updates for draft or pending requests by the owner queryset = DocumentRequest.objects.filter( employee__tenant=tenant, status__in=['DRAFT', 'PENDING'] ) if hasattr(self.request.user, 'employee_profile'): queryset = queryset.filter(employee=self.request.user.employee_profile) return queryset def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user if hasattr(self.request.user, 'employee_profile'): kwargs['employee'] = self.request.user.employee_profile return kwargs def form_valid(self, form): messages.success(self.request, 'Document request updated successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:document_request_detail', kwargs={'pk': self.object.pk}) class DocumentTemplateListView(LoginRequiredMixin, ListView): """ List all document templates (admin only). """ model = DocumentTemplate template_name = 'hr/documents/document_template_list.html' context_object_name = 'templates' paginate_by = 20 def dispatch(self, request, *args, **kwargs): if not request.user.has_perm('hr.view_documenttemplate'): messages.error(request, 'You do not have permission to view document templates.') return redirect('hr:dashboard') return super().dispatch(request, *args, **kwargs) def get_queryset(self): tenant = self.request.user.tenant queryset = DocumentTemplate.objects.filter( tenant=tenant ) # Filter by document type document_type = self.request.GET.get('document_type') if document_type: queryset = queryset.filter(document_type=document_type) # Filter by language language = self.request.GET.get('language') if language: queryset = queryset.filter(language=language) # Filter by active status is_active = self.request.GET.get('is_active') if is_active: queryset = queryset.filter(is_active=(is_active == 'true')) # Filter by default status is_default = self.request.GET.get('is_default') if is_default: queryset = queryset.filter(is_default=(is_default == 'true')) return queryset.order_by('document_type', 'language', '-is_default') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['document_types'] = DocumentRequest.DocumentType.choices context['languages'] = DocumentRequest.Language.choices return context class DocumentTemplateCreateView(LoginRequiredMixin, CreateView): """ Create a new document template (admin only). """ model = DocumentTemplate form_class = DocumentTemplateForm template_name = 'hr/documents/document_template_form.html' def dispatch(self, request, *args, **kwargs): if not request.user.has_perm('hr.add_documenttemplate'): messages.error(request, 'You do not have permission to create document templates.') return redirect('hr:document_template_list') return super().dispatch(request, *args, **kwargs) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.created_by = self.request.user messages.success(self.request, 'Document template created successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:document_template_detail', kwargs={'pk': self.object.pk}) class DocumentTemplateDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a document template. """ model = DocumentTemplate template_name = 'hr/documents/document_template_detail.html' context_object_name = 'template' def dispatch(self, request, *args, **kwargs): if not request.user.has_perm('hr.view_documenttemplate'): messages.error(request, 'You do not have permission to view document templates.') return redirect('hr:dashboard') return super().dispatch(request, *args, **kwargs) def get_queryset(self): tenant = self.request.user.tenant return DocumentTemplate.objects.filter( tenant=tenant ).select_related('created_by') def get_context_data(self, **kwargs): tenant = self.request.user.tenant context = super().get_context_data(**kwargs) template = self.get_object() # Get recent document requests using this template context['recent_requests'] = DocumentRequest.objects.filter( employee__tenant=tenant, document_type=template.document_type, language=template.language ).select_related('employee').order_by('-created_at')[:10] return context class DocumentTemplateUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing document template (admin only). """ model = DocumentTemplate form_class = DocumentTemplateForm template_name = 'hr/documents/document_template_form.html' def dispatch(self, request, *args, **kwargs): if not request.user.has_perm('hr.change_documenttemplate'): messages.error(request, 'You do not have permission to update document templates.') return redirect('hr:document_template_list') return super().dispatch(request, *args, **kwargs) def get_queryset(self): return DocumentTemplate.objects.filter(tenant=self.request.user.tenant) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): messages.success(self.request, 'Document template updated successfully.') return super().form_valid(form) def get_success_url(self): return reverse('hr:document_template_detail', kwargs={'pk': self.object.pk}) class DocumentTemplateDeleteView(LoginRequiredMixin, DeleteView): """ Delete a document template (admin only). """ model = DocumentTemplate template_name = 'hr/documents/document_template_confirm_delete.html' success_url = reverse_lazy('hr:document_template_list') def dispatch(self, request, *args, **kwargs): if not request.user.has_perm('hr.delete_documenttemplate'): messages.error(request, 'You do not have permission to delete document templates.') return redirect('hr:document_template_list') return super().dispatch(request, *args, **kwargs) def get_queryset(self): tenant = self.request.user.tenant return DocumentTemplate.objects.filter(tenant=tenant) def delete(self, request, *args, **kwargs): template = self.get_object() # Check if template is being used active_requests = DocumentRequest.objects.filter( tenant=request.user.tenant, document_type=template.document_type, language=template.language, status__in=['PENDING', 'IN_PROGRESS'] ).count() if active_requests > 0: messages.error( request, f'Cannot delete template. There are {active_requests} active document requests using this template.' ) return redirect('hr:document_template_detail', pk=template.pk) messages.success(request, f'Document template "{template.name}" deleted successfully.') return super().delete(request, *args, **kwargs) @login_required def complete_performance_review(request, review_id): review = get_object_or_404(PerformanceReview, pk=review_id) review.status = 'COMPLETED' review.completed_by = request.user review.save() messages.success(request, 'Performance review completed successfully.') return redirect('hr:performance_review_detail', pk=review.pk) @login_required def hr_stats(request): """ Return HR statistics for dashboard updates. """ context = { 'total_employees': Employee.objects.filter(tenant=request.user.tenant).count(), 'active_employees': Employee.objects.filter( tenant=request.user.tenant, employment_status='ACTIVE' ).count(), 'total_departments': Department.objects.filter(tenant=request.user.tenant).count(), 'pending_reviews': PerformanceReview.objects.filter( employee__tenant=request.user.tenant, status='IN_PROGRESS' ).count(), 'employees_clocked_in': TimeEntry.objects.filter( employee__tenant=request.user.tenant, clock_in_time__date=timezone.now().date(), clock_out_time__isnull=True ).count(), } return render(request, 'hr/partials/hr_stats.html', context) @login_required def employee_search(request): """ Search employees for HTMX updates. """ search = request.GET.get('search', '') employees = Employee.objects.filter( tenant=request.user.tenant ) if search: employees = employees.filter( Q(first_name__icontains=search) | Q(last_name__icontains=search) | Q(employee_id__icontains=search) ) employees = employees.select_related('department')[:20] return render(request, 'hr/partials/employee_list.html', { 'employees': employees }) @login_required def attendance_summary(request): """ Return attendance summary for dashboard updates. """ today = timezone.now().date() tenant = request.user.tenant # Total active employees total_active_employees = Employee.objects.filter( tenant=tenant, employment_status='ACTIVE' ).count() # Employees clocked in today (still working) clocked_in_entries = TimeEntry.objects.filter( employee__tenant=tenant, clock_in_time__date=today, clock_out_time__isnull=True ).select_related('employee', 'employee__department') employees_clocked_in = clocked_in_entries.count() # Late arrivals (clocked in after 9:00 AM) late_today = TimeEntry.objects.filter( employee__tenant=tenant, clock_in_time__date=today, clock_in_time__time__gt=time(9, 0), # Assuming 9 AM is late clock_out_time__isnull=True ).count() # Absent today (active employees not clocked in) absent_today = total_active_employees - employees_clocked_in # Total hours today (completed shifts) total_hours_today = TimeEntry.objects.filter( employee__tenant=tenant, clock_in_time__date=today, clock_out_time__isnull=False ).aggregate( total=Sum('total_hours') )['total'] # Current employees (detailed list for display) current_employees = clocked_in_entries.select_related('employee', 'employee__department').order_by( '-clock_in_time') context = { 'employees_clocked_in': employees_clocked_in, 'absent_today': absent_today, 'late_today': late_today, 'total_hours_today': total_hours_today, 'current_employees': current_employees, 'total_active_employees': total_active_employees, } return render(request, 'hr/partials/attendance_summary.html', context) @require_GET def clock_controls(request, employee_id): """Return the clock controls partial for today's state (HTMX GET).""" employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant) today = timezone.localdate() # Prefer the open entry for today; otherwise the last entry today (finished) time_entry = ( TimeEntry.objects .filter(employee=employee, work_date=today) .order_by('clock_out_time', '-clock_in_time') # open first (clock_out_time NULL), else latest finished .first() ) return render(request, 'hr/partials/clock_controls.html', { 'employee': employee, 'time_entry': time_entry, }) def _render_controls(request, employee, time_entry): html = render_to_string('hr/partials/clock_controls.html', {'employee': employee, 'time_entry': time_entry}, request=request) return HttpResponse(html) @require_POST def clock_in(request, employee_id): employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant) today = timezone.localdate() open_entry = TimeEntry.objects.filter( employee=employee, work_date=today, clock_out_time__isnull=True ).first() if open_entry: return _render_controls(request, employee, open_entry) time_entry = TimeEntry.objects.create( employee=employee, work_date=today, clock_in_time=timezone.now(), status='DRAFT' ) if request.headers.get('HX-Request'): return _render_controls(request, employee, time_entry) return JsonResponse({'success': True, 'time_entry_id': time_entry.id}) @require_POST def clock_out(request, employee_id): employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant) today = timezone.localdate() time_entry = TimeEntry.objects.filter( employee=employee, work_date=today, clock_out_time__isnull=True ).first() if not time_entry: # Re-render to default state (will show "Clock In") if request.headers.get('HX-Request'): return _render_controls(request, employee, None) return JsonResponse({'success': False, 'message': 'No active clock-in found.'}, status=400) time_entry.clock_out_time = timezone.now() time_entry.save() if request.headers.get('HX-Request'): return _render_controls(request, employee, time_entry) return JsonResponse({'success': True, 'time_entry_id': time_entry.id}) @login_required def approve_time_entry(request, entry_id): """ Approve a time entry. """ time_entry = get_object_or_404( TimeEntry, id=entry_id, employee__tenant=request.user.tenant ) if request.method == 'POST': # Check if entry is complete if not time_entry.clock_out_time: messages.error(request, 'Cannot approve incomplete time entry.') return redirect('hr:time_entry_detail', pk=time_entry.id) # Update time entry time_entry.status = 'APPROVED' time_entry.approved_by = request.user time_entry.approval_date = timezone.now() time_entry.save() messages.success(request, 'Time entry approved successfully.') # Redirect based on source next_url = request.POST.get('next', reverse('hr:time_entry_detail', kwargs={'pk': time_entry.id})) return redirect(next_url) return render(request, 'hr/time_entries/time_entry_approve.html', { 'time_entry': time_entry }) @login_required def publish_schedule(request, schedule_id): """ Publish a schedule. """ schedule = get_object_or_404( Schedule, id=schedule_id, employee__tenant=request.user.tenant ) if request.method == 'POST': # Check if schedule has assignments if not ScheduleAssignment.objects.filter(schedule=schedule).exists(): messages.error(request, 'Cannot publish empty schedule.') return redirect('hr:schedule_detail', pk=schedule.id) # Update schedule schedule.approved_by = request.user schedule.approval_date = timezone.now() schedule.save() messages.success(request, 'Schedule published successfully.') return redirect('hr:schedule_detail', pk=schedule.id) return render(request, 'hr/schedules/schedule_publish.html', { 'schedule': schedule }) @login_required def api_employee_list(request): """ API endpoint for employee list. """ employees = Employee.objects.filter( tenant=request.user.tenant ).values('employee_id', 'first_name', 'father_name', 'grandfather_name', 'last_name', 'job_title') return JsonResponse({'employees': list(employees)}) @login_required def api_department_list(request): """ API endpoint for department list. """ tenant = request.user.tenant departments = Department.objects.filter( tenant=tenant ).values('id', 'name', 'department_code') return JsonResponse({'departments': list(departments)}) # def department_tree(request): # """ # HTMX view for department tree structure. # """ # from django.db.models import Count, Q # tenant = get_tenant_from_request(request) # departments = Department.objects.filter( # tenant=tenant, # parent_department=None # ).prefetch_related('sub_departments').annotate( # employee_count=Count( # 'employee', # filter=Q(employee__employment_status='ACTIVE'), # distinct=True # ) # ).order_by('name') # # return render(request, 'hr/departments/department_tree.html', { # 'departments': departments # }) # # # @login_required # def department_children(request, department_id): # """ # HTMX endpoint to load department children dynamically. # """ # tenant = request.user.tenant # try: # parent_department = get_object_or_404( # Department, # department_id=department_id, # tenant=tenant # ) # # # Get children departments # children = Department.objects.filter( # tenant=tenant, # parent_department=parent_department # ).prefetch_related('sub_departments').order_by('name') # # # Calculate level for children (parent level + 1) # # Get parent level from request or calculate from hierarchy # parent_level = int(request.GET.get('level', 0)) # child_level = parent_level + 1 # # # Annotate employee count for each child # from django.db.models import Count, Q # children = children.annotate( # employee_count=Count( # 'employee', # filter=Q(employee__employment_status='ACTIVE'), # distinct=True # ) # ) # # # Render children using the node partial # from django.template.loader import render_to_string # html_content = "" # for child in children: # html_content += render_to_string( # 'hr/departments/partials/_department_node.html', # { # 'department': child, # 'level': child_level # }, # request=request # ) # # from django.http import HttpResponse # return HttpResponse(html_content) # # except Department.DoesNotExist: # from django.http import HttpResponse # return HttpResponse( # '