Marwan Alwali 84c1fb798e update
2025-09-08 19:52:52 +03:00

3385 lines
113 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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.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 .models import (
Employee, Department, Schedule, ScheduleAssignment,
TimeEntry, PerformanceReview, TrainingRecord
)
from .forms import (
EmployeeForm, DepartmentForm, ScheduleForm, ScheduleAssignmentForm,
TimeEntryForm, PerformanceReviewForm, TrainingRecordForm
)
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(),
'total_departments': Department.objects.filter(tenant=self.request.user.tenant).count(),
'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_number__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 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'
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):
qs = (TrainingRecord.objects
.filter(employee__tenant=self.request.user.tenant)
.select_related('employee', 'employee__department')
.order_by('-training_date', '-completion_date'))
# optional GET filters (works with the templates 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):
return TrainingRecord.objects.filter(employee__tenant=self.request.user.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')
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)
@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_number__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()
context = {
'employees_clocked_in': TimeEntry.objects.filter(
employee__tenant=request.user.tenant,
clock_in_time__date=today,
clock_out_time__isnull=True
).count(),
'total_hours_today': TimeEntry.objects.filter(
employee__tenant=request.user.tenant,
work_date=today,
clock_out_time__isnull=False
).aggregate(
total=Sum('total_hours')
)['total'] or 0,
'recent_clock_ins': TimeEntry.objects.filter(
employee__tenant=request.user.tenant,
clock_in_time__date=today
).select_related('employee').order_by('-clock_in_time')[:5],
}
return render(request, 'hr/partials/attendance_summary.html', context)
@login_required
def clock_in(request):
"""
Clock in an employee.
"""
if request.method == 'POST':
employee_id = request.POST.get('employee_id')
try:
employee = Employee.objects.get(
id=employee_id,
tenant=request.user.tenant
)
# Check if already clocked in today
today = timezone.now().date()
existing_entry = TimeEntry.objects.filter(
employee=employee,
work_date=today,
clock_out_time__isnull=True
).first()
if existing_entry:
return JsonResponse({
'success': False,
'message': 'Employee is already clocked in.'
})
# Create new time entry
time_entry = TimeEntry.objects.create(
employee=employee,
work_date=today,
clock_in_time=timezone.now(),
status='DRAFT'
)
return JsonResponse({
'success': True,
'message': f'{employee.get_full_name()} clocked in successfully.',
'time_entry_id': time_entry.id
})
except Employee.DoesNotExist:
return JsonResponse({
'success': False,
'message': 'Employee not found.'
})
return JsonResponse({'success': False, 'message': 'Invalid request.'})
@login_required
def clock_out(request):
"""
Clock out an employee.
"""
if request.method == 'POST':
employee_id = request.POST.get('employee_id')
try:
employee = Employee.objects.get(
id=employee_id,
tenant=request.user.tenant
)
# Find active time entry
today = timezone.now().date()
time_entry = TimeEntry.objects.filter(
employee=employee,
work_date=today,
clock_out_time__isnull=True
).first()
if not time_entry:
return JsonResponse({
'success': False,
'message': 'No active clock-in found for this employee.'
})
# Update time entry
time_entry.clock_out_time = timezone.now()
time_entry.save() # This will trigger the save method to calculate hours
return JsonResponse({
'success': True,
'message': f'{employee.get_full_name()} clocked out successfully.',
'time_entry_id': time_entry.id,
'hours_worked': float(time_entry.total_hours)
})
except Employee.DoesNotExist:
return JsonResponse({
'success': False,
'message': 'Employee not found.'
})
return JsonResponse({'success': False, 'message': 'Invalid request.'})
@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('id', 'first_name', 'last_name', 'employee_number', 'job_title')
return JsonResponse({'employees': list(employees)})
@login_required
def api_department_list(request):
"""
API endpoint for department list.
"""
departments = Department.objects.filter(
tenant=request.user.tenant
).values('id', 'name', 'department_code')
return JsonResponse({'departments': list(departments)})
def department_tree(request):
"""
HTMX view for department tree structure.
"""
departments = Department.objects.filter(
tenant=request.user.tenant,
parent=None
).prefetch_related('children')
return render(request, 'core/partials/department_tree.html', {
'departments': departments
})
def activate_department(request, pk):
"""
Activate a department.
"""
department = get_object_or_404(Department, department_id=pk)
department.is_active = True
department.save()
messages.success(request, f'Department "{department.name}" has been activated.')
return redirect('hr:department_detail', pk=pk)
def deactivate_department(request, pk):
"""
Deactivate a department.
"""
department = get_object_or_404(Department, department_id=pk)
department.is_active = False
department.save()
messages.success(request, f'Department "{department.name}" has been deactivated.')
return redirect('hr:department_detail', pk=pk)
def department_search(request):
"""
AJAX search for departments.
"""
query = request.GET.get('q', '')
departments = []
if query:
departments = Department.objects.filter(
tenant=request.user.tenant,
name__icontains=query
).values('department_id', 'name', 'department_type')[:10]
return JsonResponse({'departments': list(departments)})
def bulk_activate_departments(request):
"""
Bulk activate departments.
"""
if request.method == 'POST':
department_ids = request.POST.getlist('department_ids')
count = Department.objects.filter(
tenant=request.user.tenant,
department_id__in=department_ids
).update(is_active=True)
messages.success(request, f'{count} departments have been activated.')
return redirect('hr:department_list')
def bulk_deactivate_departments(request):
"""
Bulk deactivate departments.
"""
if request.method == 'POST':
department_ids = request.POST.getlist('department_ids')
count = Department.objects.filter(
tenant=request.user.tenant,
department_id__in=department_ids
).update(is_active=False)
messages.success(request, f'{count} departments have been deactivated.')
return redirect('hr:department_list')
def get_department_hierarchy(request):
"""
Get department hierarchy as JSON.
"""
departments = Department.objects.filter(
tenant=request.user.tenant,
is_active=True
).select_related('parent')
def build_tree(parent=None):
children = []
for dept in departments:
if dept.parent == parent:
children.append({
'id': str(dept.department_id),
'name': dept.name,
'type': dept.department_type,
'children': build_tree(dept)
})
return children
hierarchy = build_tree()
return JsonResponse({'hierarchy': hierarchy})
@login_required
def assign_department_head(request, pk):
"""
Assign a department head to a department.
"""
department = get_object_or_404(Department, pk=pk, tenant=request.user.tenant)
if request.method == 'POST':
user_id = request.POST.get('user_id')
if user_id:
try:
user = User.objects.get(id=user_id, tenant=request.user.tenant)
# Remove current department head if exists
if department.department_head:
old_head = department.department_head
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Removed department head from {department.name}',
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'old_department_head_id': old_head.id,
'old_department_head_name': old_head.get_full_name()
}
)
# Assign new department head
department.department_head = user
department.save()
# Log the assignment
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Assigned department head to {department.name}',
description=f'Assigned {user.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'new_department_head_id': user.id,
'new_department_head_name': user.get_full_name()
}
)
messages.success(
request,
f'{user.get_full_name()} has been assigned as head of {department.name}.'
)
return redirect('core:department_detail', pk=department.pk)
except User.DoesNotExist:
messages.error(request, 'Selected user not found.')
else:
# Remove department head
if department.department_head:
old_head = department.department_head
department.department_head = None
department.save()
# Log the removal
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Removed department head from {department.name}',
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'removed_department_head_id': old_head.id,
'removed_department_head_name': old_head.get_full_name()
}
)
messages.success(
request,
f'Department head has been removed from {department.name}.'
)
else:
messages.info(request, 'No department head was assigned.')
return redirect('core:department_detail', pk=department.pk)
# Get eligible users (staff members who can be department heads)
eligible_users = User.objects.filter(
tenant=request.user.tenant,
is_active=True,
is_staff=True
).exclude(
id=department.department_head.id if department.department_head else None
).order_by('first_name', 'last_name')
context = {
'department': department,
'eligible_users': eligible_users,
'current_head': department.department_head,
}
return render(request, 'hr/departments/assign_department_head.html', context)
# Query patterns to use if needed
# # All upcoming sessions for a tenant (next 30 days)
# TrainingSession.objects.filter(
# tenant=request.user.tenant, start_at__gte=timezone.now(),
# start_at__lte=timezone.now() + timedelta(days=30)
# ).select_related('program', 'instructor')
#
# # Employees due for renewal in 30 days
# TrainingCertificates.objects.filter(
# tenant=request.user.tenant,
# expiry_date__lte=date.today() + timedelta(days=30),
# expiry_date__gte=date.today()
# ).select_related('employee', 'program')
#
# # Enroll an employee, respecting capacity
# session = TrainingSession.objects.select_for_update().get(pk=session_pk, tenant=tenant)
# if session.capacity and session.enrollments.count() >= session.capacity:
# status = 'WAITLISTED'
# else:
# status = 'SCHEDULED'
# enrollment = TrainingRecord.objects.create(
# tenant=tenant, employee=emp, program=session.program, session=session,
# status=status, created_by=request.user
# )
#
# # Mark completion + pass (auto-certificate will fire via signal)
# enrollment.status = 'COMPLETED'
# enrollment.passed = True
# enrollment.completion_date = date.today()
# enrollment.credits_earned = enrollment.hours
# enrollment.save()
#
# from django.shortcuts import render, redirect, get_object_or_404
# from django.views.generic import (
# ListView, DetailView, CreateView, UpdateView, DeleteView
# )
# from django.urls import reverse, reverse_lazy
# from django.contrib.auth.decorators import login_required
# from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
# from django.contrib import messages
# from django.utils import timezone
# from django.http import HttpResponse, JsonResponse
# from django.db.models import Q, Count, Sum, Avg, F, ExpressionWrapper, fields
# from django.template.loader import render_to_string
# from django.core.paginator import Paginator
#
# from .models import (
# Department, Employee, Schedule, ScheduleAssignment,
# TimeEntry, PerformanceReview, TrainingRecord
# )
# from .forms import (
# DepartmentForm, EmployeeForm, ScheduleForm, ScheduleAssignmentForm,
# TimeEntryForm, PerformanceReviewForm, TrainingRecordForm,
# HRSearchForm, TimeEntrySearchForm
# )
#
#
# class DashboardView(LoginRequiredMixin, ListView):
# """
# HR dashboard view displaying summary statistics.
# """
# template_name = 'hr/dashboard.html'
# context_object_name = 'employees'
# paginate_by = 10
#
# def get_queryset(self):
# """
# Get recent employees as primary queryset.
# """
# return Employee.objects.filter(
# tenant=self.request.user.tenant,
# is_active=True
# ).order_by('-hire_date')[:10]
#
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# tenant = self.request.user.tenant
#
# # Employee statistics
# context['total_employees'] = Employee.objects.filter(
# tenant=tenant
# ).count()
#
# context['active_employees'] = Employee.objects.filter(
# tenant=tenant,
# is_active=True
# ).count()
#
# context['department_stats'] = Department.objects.filter(
# tenant=tenant
# ).annotate(
# employee_count=Count('employees', filter=Q(employees__is_active=True))
# ).order_by('-employee_count')[:5]
#
# # Recent hires
# context['recent_hires'] = Employee.objects.filter(
# tenant=tenant
# ).order_by('-hire_date')[:5]
#
# # Upcoming reviews
# context['upcoming_reviews'] = PerformanceReview.objects.filter(
# tenant=tenant,
# status='PENDING',
# scheduled_date__gte=timezone.now().date()
# ).order_by('scheduled_date')[:5]
#
# # Employees on leave
# context['employees_on_leave'] = Employee.objects.filter(
# tenant=tenant,
# employment_status='LEAVE'
# ).order_by('last_name')
#
# # Training expiring soon
# context['expiring_trainings'] = TrainingRecord.objects.filter(
# tenant=tenant,
# status='COMPLETED',
# expiry_date__gte=timezone.now().date(),
# expiry_date__lte=timezone.now().date() + timezone.timedelta(days=30)
# ).order_by('expiry_date')[:5]
#
# # Time entry approvals pending
# context['pending_time_approvals'] = TimeEntry.objects.filter(
# tenant=tenant,
# is_approved=False,
# entry_date__gte=timezone.now().date() - timezone.timedelta(days=14)
# ).count()
#
# # Search form
# context['search_form'] = HRSearchForm(tenant=tenant)
#
# return context
#
#
# class EmployeeListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
# """
# List view for employees.
# """
# model = Employee
# template_name = 'hr/employee_list.html'
# context_object_name = 'employees'
# paginate_by = 20
# permission_required = 'hr.view_employee'
#
# def get_queryset(self):
# """
# Filter employees by tenant and apply search filters.
# """
# queryset = Employee.objects.filter(tenant=self.request.user.tenant)
#
# # Apply search filters
# form = HRSearchForm(self.request.GET, tenant=self.request.user.tenant)
# if form.is_valid():
# data = form.cleaned_data
#
# # Search term
# if data.get('search'):
# search_term = data['search']
# queryset = queryset.filter(
# Q(first_name__icontains=search_term) |
# Q(last_name__icontains=search_term) |
# Q(employee_number__icontains=search_term) |
# Q(position__icontains=search_term) |
# Q(personal_email__icontains=search_term) |
# Q(work_email__icontains=search_term)
# )
#
# # Department filter
# if data.get('department'):
# queryset = queryset.filter(department=data['department'])
#
# # Employment status filter
# if data.get('employment_status'):
# queryset = queryset.filter(employment_status=data['employment_status'])
#
# # Employment type filter
# if data.get('employment_type'):
# queryset = queryset.filter(employment_type=data['employment_type'])
#
# # Position filter
# if data.get('position'):
# queryset = queryset.filter(position__icontains=data['position'])
#
# # Hire date range
# if data.get('hire_date_from'):
# queryset = queryset.filter(hire_date__gte=data['hire_date_from'])
#
# if data.get('hire_date_to'):
# queryset = queryset.filter(hire_date__lte=data['hire_date_to'])
#
# # Active status
# if data.get('is_active') == 'True':
# queryset = queryset.filter(is_active=True)
# elif data.get('is_active') == 'False':
# queryset = queryset.filter(is_active=False)
#
# return queryset.select_related('department', 'supervisor')
#
# def get_context_data(self, **kwargs):
# """
# Add search form to context.
# """
# context = super().get_context_data(**kwargs)
# context['search_form'] = HRSearchForm(
# self.request.GET or None,
# tenant=self.request.user.tenant
# )
# return context
#
#
# class EmployeeDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# Detail view for employees.
# """
# model = Employee
# template_name = 'hr/employee_detail.html'
# context_object_name = 'employee'
# permission_required = 'hr.view_employee'
#
# def get_queryset(self):
# """
# Filter employee by tenant.
# """
# return Employee.objects.filter(
# tenant=self.request.user.tenant
# ).select_related(
# 'department', 'supervisor', 'user', 'created_by'
# )
#
# def get_context_data(self, **kwargs):
# """
# Add related objects to context.
# """
# context = super().get_context_data(**kwargs)
# employee = self.object
#
# # Current schedule
# context['current_schedule'] = Schedule.objects.filter(
# employee=employee,
# is_current=True,
# is_active=True
# ).first()
#
# # Recent time entries
# context['recent_time_entries'] = TimeEntry.objects.filter(
# employee=employee
# ).order_by('-entry_date', '-created_at')[:10]
#
# # Performance reviews
# context['performance_reviews'] = PerformanceReview.objects.filter(
# employee=employee
# ).order_by('-scheduled_date')[:5]
#
# # Training records
# context['training_records'] = TrainingRecord.objects.filter(
# employee=employee
# ).order_by('-start_date')[:10]
#
# # Subordinates
# context['subordinates'] = Employee.objects.filter(
# supervisor=employee,
# is_active=True
# ).order_by('last_name', 'first_name')
#
# return context
#
#
# class EmployeeCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
# """
# Create view for employees.
# """
# model = Employee
# form_class = EmployeeForm
# template_name = 'hr/employee_form.html'
# permission_required = 'hr.add_employee'
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Set tenant and user before saving.
# """
# form.instance.tenant = self.request.user.tenant
# form.instance.created_by = self.request.user
#
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Employee {form.instance.get_full_name()} created successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to employee detail view.
# """
# return reverse('hr:employee_detail', kwargs={'pk': self.object.pk})
#
#
# class EmployeeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# """
# Update view for employees.
# """
# model = Employee
# form_class = EmployeeForm
# template_name = 'hr/employee_form.html'
# permission_required = 'hr.change_employee'
#
# def get_queryset(self):
# """
# Filter employee by tenant.
# """
# return Employee.objects.filter(tenant=self.request.user.tenant)
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Handle successful form validation.
# """
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Employee {form.instance.get_full_name()} updated successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to employee detail view.
# """
# return reverse('hr:employee_detail', kwargs={'pk': self.object.pk})
#
#
# class EmployeeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
# """
# Delete view for employees.
# """
# model = Employee
# template_name = 'hr/employee_confirm_delete.html'
# permission_required = 'hr.delete_employee'
#
# def get_queryset(self):
# """
# Filter employee by tenant.
# """
# return Employee.objects.filter(tenant=self.request.user.tenant)
#
# def delete(self, request, *args, **kwargs):
# """
# Override delete to show success message.
# """
# self.object = self.get_object()
# name = self.object.get_full_name()
#
# # Check if employee can be safely deleted
# if self.object.managed_departments.exists():
# messages.error(
# request,
# f"Cannot delete {name} because they are assigned as a department manager."
# )
# return redirect('hr:employee_detail', pk=self.object.pk)
#
# if self.object.subordinates.exists():
# messages.error(
# request,
# f"Cannot delete {name} because they are assigned as a supervisor."
# )
# return redirect('hr:employee_detail', pk=self.object.pk)
#
# success_url = self.get_success_url()
# self.object.delete()
#
# messages.success(request, f"Employee {name} deleted successfully.")
#
# return redirect(success_url)
#
# def get_success_url(self):
# """
# Redirect to employee list view.
# """
# return reverse_lazy('hr:employee_list')
#
#
# class DepartmentListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
# """
# List view for departments.
# """
# model = Department
# template_name = 'hr/department_list.html'
# context_object_name = 'departments'
# paginate_by = 20
# permission_required = 'hr.view_department'
#
# def get_queryset(self):
# """
# Filter departments by tenant.
# """
# queryset = Department.objects.filter(tenant=self.request.user.tenant)
#
# # Apply search filters
# search = self.request.GET.get('search', '')
# if search:
# queryset = queryset.filter(
# Q(name__icontains=search) |
# Q(code__icontains=search) |
# Q(description__icontains=search)
# )
#
# department_type = self.request.GET.get('department_type', '')
# if department_type:
# queryset = queryset.filter(department_type=department_type)
#
# is_active = self.request.GET.get('is_active', '')
# if is_active == 'True':
# queryset = queryset.filter(is_active=True)
# elif is_active == 'False':
# queryset = queryset.filter(is_active=False)
#
# return queryset.select_related('manager', 'parent_department')
#
# def get_context_data(self, **kwargs):
# """
# Add filter choices to context.
# """
# context = super().get_context_data(**kwargs)
# context['search'] = self.request.GET.get('search', '')
# context['department_types'] = Department.DEPARTMENT_TYPES
# context['selected_type'] = self.request.GET.get('department_type', '')
# context['selected_active'] = self.request.GET.get('is_active', '')
# return context
#
#
# class DepartmentDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# Detail view for departments.
# """
# model = Department
# template_name = 'hr/department_detail.html'
# context_object_name = 'department'
# permission_required = 'hr.view_department'
#
# def get_queryset(self):
# """
# Filter department by tenant.
# """
# return Department.objects.filter(
# tenant=self.request.user.tenant
# ).select_related(
# 'manager', 'parent_department', 'created_by'
# )
#
# def get_context_data(self, **kwargs):
# """
# Add related objects to context.
# """
# context = super().get_context_data(**kwargs)
# department = self.object
#
# # Employees in department
# context['employees'] = Employee.objects.filter(
# department=department,
# is_active=True
# ).order_by('last_name', 'first_name')
#
# # Sub-departments
# context['sub_departments'] = Department.objects.filter(
# parent_department=department,
# is_active=True
# ).order_by('name')
#
# return context
#
#
# class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
# """
# Create view for departments.
# """
# model = Department
# form_class = DepartmentForm
# template_name = 'hr/department_form.html'
# permission_required = 'hr.add_department'
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Set tenant and user before saving.
# """
# form.instance.tenant = self.request.user.tenant
# form.instance.created_by = self.request.user
#
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Department {form.instance.name} created successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to department detail view.
# """
# return reverse('hr:department_detail', kwargs={'pk': self.object.pk})
#
#
# class DepartmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# """
# Update view for departments.
# """
# model = Department
# form_class = DepartmentForm
# template_name = 'hr/department_form.html'
# permission_required = 'hr.change_department'
#
# def get_queryset(self):
# """
# Filter department by tenant.
# """
# return Department.objects.filter(tenant=self.request.user.tenant)
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Handle successful form validation.
# """
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Department {form.instance.name} updated successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to department detail view.
# """
# return reverse('hr:department_detail', kwargs={'pk': self.object.pk})
#
#
# class DepartmentDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
# """
# Delete view for departments.
# """
# model = Department
# template_name = 'hr/department_confirm_delete.html'
# permission_required = 'hr.delete_department'
#
# def get_queryset(self):
# """
# Filter department by tenant.
# """
# return Department.objects.filter(tenant=self.request.user.tenant)
#
# def delete(self, request, *args, **kwargs):
# """
# Override delete to show success message.
# """
# self.object = self.get_object()
# name = self.object.name
#
# # Check if department can be safely deleted
# if self.object.employees.exists():
# messages.error(
# request,
# f"Cannot delete {name} because it has employees assigned to it."
# )
# return redirect('hr:department_detail', pk=self.object.pk)
#
# if self.object.sub_departments.exists():
# messages.error(
# request,
# f"Cannot delete {name} because it has sub-departments assigned to it."
# )
# return redirect('hr:department_detail', pk=self.object.pk)
#
# success_url = self.get_success_url()
# self.object.delete()
#
# messages.success(request, f"Department {name} deleted successfully.")
#
# return redirect(success_url)
#
# def get_success_url(self):
# """
# Redirect to department list view.
# """
# return reverse_lazy('hr:department_list')
#
#
# class ScheduleListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
# """
# List view for schedules.
# """
# model = Schedule
# template_name = 'hr/schedule_list.html'
# context_object_name = 'schedules'
# paginate_by = 20
# permission_required = 'hr.view_schedule'
#
# def get_queryset(self):
# """
# Filter schedules by tenant and apply search filters.
# """
# queryset = Schedule.objects.filter(
# employee__tenant=self.request.user.tenant
# )
#
# # Apply search filters
# search = self.request.GET.get('search', '')
# if search:
# queryset = queryset.filter(
# Q(name__icontains=search) |
# Q(description__icontains=search) |
# Q(employee__first_name__icontains=search) |
# Q(employee__last_name__icontains=search) |
# Q(employee__employee_number__icontains=search)
# )
#
# employee_id = self.request.GET.get('employee', '')
# if employee_id:
# queryset = queryset.filter(employee_id=employee_id)
#
# schedule_type = self.request.GET.get('schedule_type', '')
# if schedule_type:
# queryset = queryset.filter(schedule_type=schedule_type)
#
# is_active = self.request.GET.get('is_active', '')
# if is_active == 'True':
# queryset = queryset.filter(is_active=True)
# elif is_active == 'False':
# queryset = queryset.filter(is_active=False)
#
# is_current = self.request.GET.get('is_current', '')
# if is_current == 'True':
# queryset = queryset.filter(is_current=True)
#
# return queryset.select_related('employee', 'approved_by')
#
# def get_context_data(self, **kwargs):
# """
# Add filter choices to context.
# """
# context = super().get_context_data(**kwargs)
# tenant = self.request.user.tenant
#
# context['search'] = self.request.GET.get('search', '')
# context['employees'] = Employee.objects.filter(
# tenant=tenant,
# is_active=True
# ).order_by('last_name', 'first_name')
# context['schedule_types'] = Schedule.SCHEDULE_TYPES
# context['selected_employee'] = self.request.GET.get('employee', '')
# context['selected_type'] = self.request.GET.get('schedule_type', '')
# context['selected_active'] = self.request.GET.get('is_active', '')
# context['selected_current'] = self.request.GET.get('is_current', '')
#
# return context
#
#
# class ScheduleDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# Detail view for schedules.
# """
# model = Schedule
# template_name = 'hr/schedule_detail.html'
# context_object_name = 'schedule'
# permission_required = 'hr.view_schedule'
#
# def get_queryset(self):
# """
# Filter schedule by tenant.
# """
# return Schedule.objects.filter(
# employee__tenant=self.request.user.tenant
# ).select_related(
# 'employee', 'approved_by', 'created_by'
# )
#
# def get_context_data(self, **kwargs):
# """
# Add related objects to context.
# """
# context = super().get_context_data(**kwargs)
# schedule = self.object
#
# # Assignments for this schedule
# context['assignments'] = ScheduleAssignment.objects.filter(
# schedule=schedule
# ).order_by('assignment_date', 'start_time')
#
# # Group assignments by week
# assignments_by_week = {}
# for assignment in context['assignments']:
# # Get week start date (Monday)
# week_start = assignment.assignment_date - timezone.timedelta(
# days=assignment.assignment_date.weekday()
# )
# week_key = week_start.strftime('%Y-%m-%d')
#
# if week_key not in assignments_by_week:
# assignments_by_week[week_key] = []
#
# assignments_by_week[week_key].append(assignment)
#
# context['assignments_by_week'] = assignments_by_week
#
# return context
#
#
# class ScheduleCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
# """
# Create view for schedules.
# """
# model = Schedule
# form_class = ScheduleForm
# template_name = 'hr/schedule_form.html'
# permission_required = 'hr.add_schedule'
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
#
# # Pre-select employee if provided in GET parameters
# employee_id = self.request.GET.get('employee', None)
# if employee_id:
# kwargs['initial'] = kwargs.get('initial', {})
# kwargs['initial']['employee'] = employee_id
#
# return kwargs
#
# def form_valid(self, form):
# """
# Set tenant and user before saving.
# """
# form.instance.tenant = self.request.user.tenant
# form.instance.created_by = self.request.user
#
# # Set approval information
# form.instance.approved_by = self.request.user
# form.instance.approval_date = timezone.now()
#
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Schedule '{form.instance.name}' for {form.instance.employee.get_full_name()} created successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to schedule detail view.
# """
# return reverse('hr:schedule_detail', kwargs={'pk': self.object.pk})
#
#
# class ScheduleUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# """
# Update view for schedules.
# """
# model = Schedule
# form_class = ScheduleForm
# template_name = 'hr/schedule_form.html'
# permission_required = 'hr.change_schedule'
#
# def get_queryset(self):
# """
# Filter schedule by tenant.
# """
# return Schedule.objects.filter(
# employee__tenant=self.request.user.tenant
# )
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Handle successful form validation.
# """
# # If is_current status changed, update approval information
# if 'is_current' in form.changed_data and form.instance.is_current:
# form.instance.approved_by = self.request.user
# form.instance.approval_date = timezone.now()
#
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Schedule '{form.instance.name}' updated successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to schedule detail view.
# """
# return reverse('hr:schedule_detail', kwargs={'pk': self.object.pk})
#
#
# class ScheduleDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
# """
# Delete view for schedules.
# """
# model = Schedule
# template_name = 'hr/schedule_confirm_delete.html'
# permission_required = 'hr.delete_schedule'
#
# def get_queryset(self):
# """
# Filter schedule by tenant.
# """
# return Schedule.objects.filter(
# employee__tenant=self.request.user.tenant
# )
#
# def delete(self, request, *args, **kwargs):
# """
# Override delete to show success message.
# """
# self.object = self.get_object()
# employee_name = self.object.employee.get_full_name()
# schedule_name = self.object.name
#
# # Check if schedule can be safely deleted
# if self.object.is_current:
# messages.error(
# request,
# f"Cannot delete schedule '{schedule_name}' because it is the current schedule for {employee_name}."
# )
# return redirect('hr:schedule_detail', pk=self.object.pk)
#
# success_url = self.get_success_url()
# self.object.delete()
#
# messages.success(
# request,
# f"Schedule '{schedule_name}' for {employee_name} deleted successfully."
# )
#
# return redirect(success_url)
#
# def get_success_url(self):
# """
# Redirect to schedule list view.
# """
# return reverse_lazy('hr:schedule_list')
#
#
# class TimeEntryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
# """
# List view for time entries.
# """
# model = TimeEntry
# template_name = 'hr/time_entry_list.html'
# context_object_name = 'time_entries'
# paginate_by = 20
# permission_required = 'hr.view_timeentry'
#
# def get_queryset(self):
# """
# Filter time entries by tenant and apply search filters.
# """
# queryset = TimeEntry.objects.filter(tenant=self.request.user.tenant)
#
# # Apply search filters
# form = TimeEntrySearchForm(self.request.GET, tenant=self.request.user.tenant)
# if form.is_valid():
# data = form.cleaned_data
#
# # Search term
# if data.get('search'):
# search_term = data['search']
# queryset = queryset.filter(
# Q(employee__first_name__icontains=search_term) |
# Q(employee__last_name__icontains=search_term) |
# Q(employee__employee_number__icontains=search_term) |
# Q(description__icontains=search_term)
# )
#
# # Employee filter
# if data.get('employee'):
# queryset = queryset.filter(employee=data['employee'])
#
# # Department filter
# if data.get('department'):
# queryset = queryset.filter(department=data['department'])
#
# # Entry type filter
# if data.get('entry_type'):
# queryset = queryset.filter(entry_type=data['entry_type'])
#
# # Entry date range
# if data.get('entry_date_from'):
# queryset = queryset.filter(entry_date__gte=data['entry_date_from'])
#
# if data.get('entry_date_to'):
# queryset = queryset.filter(entry_date__lte=data['entry_date_to'])
#
# # Approval status
# if data.get('is_approved') == 'True':
# queryset = queryset.filter(is_approved=True)
# elif data.get('is_approved') == 'False':
# queryset = queryset.filter(is_approved=False)
#
# # Payment status
# if data.get('is_paid') == 'True':
# queryset = queryset.filter(is_paid=True)
# elif data.get('is_paid') == 'False':
# queryset = queryset.filter(is_paid=False)
#
# return queryset.select_related(
# 'employee', 'department', 'approved_by'
# ).order_by('-entry_date', '-updated_at')
#
# def get_context_data(self, **kwargs):
# """
# Add search form to context.
# """
# context = super().get_context_data(**kwargs)
# context['search_form'] = TimeEntrySearchForm(
# self.request.GET or None,
# tenant=self.request.user.tenant
# )
#
# # Calculate totals
# entries = self.object_list
# context['total_hours'] = sum(entry.total_hours for entry in entries)
# context['total_approved'] = sum(1 for entry in entries if entry.is_approved)
# context['total_pending'] = sum(1 for entry in entries if not entry.is_approved)
#
# return context
#
#
# class TimeEntryDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# Detail view for time entries.
# """
# model = TimeEntry
# template_name = 'hr/time_entry_detail.html'
# context_object_name = 'time_entry'
# permission_required = 'hr.view_timeentry'
#
# def get_queryset(self):
# """
# Filter time entry by tenant.
# """
# return TimeEntry.objects.filter(
# tenant=self.request.user.tenant
# ).select_related(
# 'employee', 'department', 'approved_by', 'created_by'
# )
#
#
# class TimeEntryCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
# """
# Create view for time entries.
# """
# model = TimeEntry
# form_class = TimeEntryForm
# template_name = 'hr/time_entry_form.html'
# permission_required = 'hr.add_timeentry'
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
#
# # Pre-select employee and date if provided in GET parameters
# initial = {}
# employee_id = self.request.GET.get('employee', None)
# if employee_id:
# initial['employee'] = employee_id
#
# entry_date = self.request.GET.get('entry_date', None)
# if entry_date:
# try:
# initial['entry_date'] = timezone.datetime.strptime(
# entry_date, '%Y-%m-%d'
# ).date()
# except (ValueError, TypeError):
# pass
#
# if initial:
# kwargs['initial'] = initial
#
# return kwargs
#
# def form_valid(self, form):
# """
# Set tenant and user before saving.
# """
# form.instance.tenant = self.request.user.tenant
# form.instance.created_by = self.request.user
#
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Time entry for {form.instance.employee.get_full_name()} on {form.instance.entry_date} created successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to time entry list view or employee detail based on origin.
# """
# if 'employee' in self.request.GET:
# return reverse('hr:employee_detail', kwargs={'pk': self.request.GET['employee']})
# return reverse('hr:time_entry_list')
#
#
# class TimeEntryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# """
# Update view for time entries.
# """
# model = TimeEntry
# form_class = TimeEntryForm
# template_name = 'hr/time_entry_form.html'
# permission_required = 'hr.change_timeentry'
#
# def get_queryset(self):
# """
# Filter time entry by tenant.
# """
# return TimeEntry.objects.filter(tenant=self.request.user.tenant)
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Handle successful form validation.
# """
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Time entry for {form.instance.employee.get_full_name()} on {form.instance.entry_date} updated successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to time entry detail view.
# """
# return reverse('hr:time_entry_detail', kwargs={'pk': self.object.pk})
#
#
# @login_required
# def approve_time_entry(request, pk):
# """
# Approve a time entry.
# """
# time_entry = get_object_or_404(
# TimeEntry,
# pk=pk,
# tenant=request.user.tenant
# )
#
# if request.method == 'POST':
# if not request.user.has_perm('hr.approve_timeentry'):
# messages.error(request, "You don't have permission to approve time entries.")
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
#
# time_entry.is_approved = True
# time_entry.approved_by = request.user
# time_entry.approval_date = timezone.now()
# time_entry.save()
#
# messages.success(
# request,
# f"Time entry for {time_entry.employee.get_full_name()} on {time_entry.entry_date} approved."
# )
#
# redirect_url = request.POST.get('next', reverse('hr:time_entry_detail', kwargs={'pk': time_entry.pk}))
# return redirect(redirect_url)
#
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
#
#
# @login_required
# def mark_time_entry_paid(request, pk):
# """
# Mark a time entry as paid.
# """
# time_entry = get_object_or_404(
# TimeEntry,
# pk=pk,
# tenant=request.user.tenant
# )
#
# if request.method == 'POST':
# if not request.user.has_perm('hr.change_timeentry'):
# messages.error(request, "You don't have permission to mark time entries as paid.")
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
#
# time_entry.is_paid = True
# time_entry.payment_date = timezone.now().date()
# time_entry.save()
#
# messages.success(
# request,
# f"Time entry for {time_entry.employee.get_full_name()} on {time_entry.entry_date} marked as paid."
# )
#
# redirect_url = request.POST.get('next', reverse('hr:time_entry_detail', kwargs={'pk': time_entry.pk}))
# return redirect(redirect_url)
#
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
#
#
# @login_required
# def bulk_approve_time_entries(request):
# """
# Bulk approve time entries.
# """
# if not request.user.has_perm('hr.approve_timeentry'):
# messages.error(request, "You don't have permission to approve time entries.")
# return redirect('hr:time_entry_list')
#
# if request.method == 'POST':
# entry_ids = request.POST.getlist('entry_ids')
#
# if not entry_ids:
# messages.warning(request, "No time entries selected for approval.")
# return redirect('hr:time_entry_list')
#
# # Get entries that belong to this tenant
# entries = TimeEntry.objects.filter(
# tenant=request.user.tenant,
# pk__in=entry_ids,
# is_approved=False
# )
#
# # Update all entries
# updated_count = entries.update(
# is_approved=True,
# approved_by=request.user,
# approval_date=timezone.now()
# )
#
# messages.success(request, f"{updated_count} time entries approved successfully.")
#
# redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
# return redirect(redirect_url)
#
# return redirect('hr:time_entry_list')
#
#
# @login_required
# def bulk_mark_time_entries_paid(request):
# """
# Bulk mark time entries as paid.
# """
# if not request.user.has_perm('hr.change_timeentry'):
# messages.error(request, "You don't have permission to mark time entries as paid.")
# return redirect('hr:time_entry_list')
#
# if request.method == 'POST':
# entry_ids = request.POST.getlist('entry_ids')
#
# if not entry_ids:
# messages.warning(request, "No time entries selected for payment.")
# return redirect('hr:time_entry_list')
#
# # Get entries that belong to this tenant
# entries = TimeEntry.objects.filter(
# tenant=request.user.tenant,
# pk__in=entry_ids,
# is_approved=True,
# is_paid=False
# )
#
# # Update all entries
# updated_count = entries.update(
# is_paid=True,
# payment_date=timezone.now().date()
# )
#
# messages.success(request, f"{updated_count} time entries marked as paid successfully.")
#
# redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
# return redirect(redirect_url)
#
# return redirect('hr:time_entry_list')
#
#
# class PerformanceReviewListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
# """
# List view for performance reviews.
# """
# model = PerformanceReview
# template_name = 'hr/performance_review_list.html'
# context_object_name = 'reviews'
# paginate_by = 20
# permission_required = 'hr.view_performancereview'
#
# def get_queryset(self):
# """
# Filter reviews by tenant and apply search filters.
# """
# queryset = PerformanceReview.objects.filter(tenant=self.request.user.tenant)
#
# # Apply search filters
# 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_number__icontains=search) |
# Q(strengths__icontains=search) |
# Q(areas_for_improvement__icontains=search)
# )
#
# employee_id = self.request.GET.get('employee', '')
# if employee_id:
# queryset = queryset.filter(employee_id=employee_id)
#
# review_type = self.request.GET.get('review_type', '')
# if review_type:
# queryset = queryset.filter(review_type=review_type)
#
# status = self.request.GET.get('status', '')
# if status:
# queryset = queryset.filter(status=status)
#
# # Date range filters
# scheduled_from = self.request.GET.get('scheduled_from', '')
# if scheduled_from:
# try:
# scheduled_from = timezone.datetime.strptime(scheduled_from, '%Y-%m-%d').date()
# queryset = queryset.filter(scheduled_date__gte=scheduled_from)
# except (ValueError, TypeError):
# pass
#
# scheduled_to = self.request.GET.get('scheduled_to', '')
# if scheduled_to:
# try:
# scheduled_to = timezone.datetime.strptime(scheduled_to, '%Y-%m-%d').date()
# queryset = queryset.filter(scheduled_date__lte=scheduled_to)
# except (ValueError, TypeError):
# pass
#
# # Rating filter
# rating = self.request.GET.get('rating', '')
# if rating and rating.isdigit():
# queryset = queryset.filter(performance_rating=int(rating))
#
# return queryset.select_related('employee', 'reviewer')
#
# def get_context_data(self, **kwargs):
# """
# Add filter choices to context.
# """
# context = super().get_context_data(**kwargs)
# tenant = self.request.user.tenant
#
# context['search'] = self.request.GET.get('search', '')
# context['employees'] = Employee.objects.filter(
# tenant=tenant
# ).order_by('last_name', 'first_name')
# context['review_types'] = PerformanceReview.REVIEW_TYPES
# context['review_statuses'] = PerformanceReview.REVIEW_STATUS
# context['selected_employee'] = self.request.GET.get('employee', '')
# context['selected_type'] = self.request.GET.get('review_type', '')
# context['selected_status'] = self.request.GET.get('status', '')
# context['scheduled_from'] = self.request.GET.get('scheduled_from', '')
# context['scheduled_to'] = self.request.GET.get('scheduled_to', '')
# context['selected_rating'] = self.request.GET.get('rating', '')
#
# return context
#
#
# class PerformanceReviewDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# Detail view for performance reviews.
# """
# model = PerformanceReview
# template_name = 'hr/performance_review_detail.html'
# context_object_name = 'review'
# permission_required = 'hr.view_performancereview'
#
# def get_queryset(self):
# """
# Filter review by tenant.
# """
# return PerformanceReview.objects.filter(
# tenant=self.request.user.tenant
# ).select_related(
# 'employee', 'reviewer', 'created_by'
# )
#
#
# class PerformanceReviewCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
# """
# Create view for performance reviews.
# """
# model = PerformanceReview
# form_class = PerformanceReviewForm
# template_name = 'hr/performance_review_form.html'
# permission_required = 'hr.add_performancereview'
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
#
# # Pre-select employee if provided in GET parameters
# employee_id = self.request.GET.get('employee', None)
# if employee_id:
# kwargs['initial'] = kwargs.get('initial', {})
# kwargs['initial']['employee'] = employee_id
# kwargs['initial']['reviewer'] = self.request.user.id
#
# return kwargs
#
# def form_valid(self, form):
# """
# Set tenant and user before saving.
# """
# form.instance.tenant = self.request.user.tenant
# form.instance.created_by = self.request.user
#
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Performance review for {form.instance.employee.get_full_name()} created successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to review detail view.
# """
# return reverse('hr:performance_review_detail', kwargs={'pk': self.object.pk})
#
#
# class PerformanceReviewUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# """
# Update view for performance reviews.
# """
# model = PerformanceReview
# form_class = PerformanceReviewForm
# template_name = 'hr/performance_review_form.html'
# permission_required = 'hr.change_performancereview'
#
# def get_queryset(self):
# """
# Filter review by tenant.
# """
# return PerformanceReview.objects.filter(tenant=self.request.user.tenant)
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Handle successful form validation.
# """
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Performance review for {form.instance.employee.get_full_name()} updated successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to review detail view.
# """
# return reverse('hr:performance_review_detail', kwargs={'pk': self.object.pk})
#
#
# @login_required
# def acknowledge_review(request, pk):
# """
# Acknowledge a performance review.
# """
# review = get_object_or_404(
# PerformanceReview,
# pk=pk,
# tenant=request.user.tenant
# )
#
# # Check if the user is the employee being reviewed
# if hasattr(request.user, 'employee_profile') and request.user.employee_profile == review.employee:
# if request.method == 'POST':
# review.is_acknowledged = True
# review.acknowledgment_date = timezone.now()
# review.save()
#
# messages.success(request, "You have acknowledged this performance review.")
#
# redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
# return redirect(redirect_url)
# else:
# messages.error(request, "Only the employee being reviewed can acknowledge this review.")
#
# return redirect('hr:performance_review_detail', pk=review.pk)
#
#
# @login_required
# def complete_review(request, pk):
# """
# Mark a performance review as completed.
# """
# review = get_object_or_404(
# PerformanceReview,
# pk=pk,
# tenant=request.user.tenant
# )
#
# if request.method == 'POST':
# if not request.user.has_perm('hr.change_performancereview'):
# messages.error(request, "You don't have permission to complete reviews.")
# return redirect('hr:performance_review_detail', pk=review.pk)
#
# # Check if reviewer is the one completing the review
# if review.reviewer != request.user:
# messages.warning(request, "Only the assigned reviewer should complete this review.")
#
# # Update review status
# review.status = 'COMPLETED'
# review.completion_date = timezone.now().date()
# review.save()
#
# # Update employee's last and next review dates
# employee = review.employee
# employee.last_review_date = review.completion_date
# employee.next_review_date = review.next_review_date
# employee.save()
#
# messages.success(
# request,
# f"Performance review for {review.employee.get_full_name()} marked as completed."
# )
#
# redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
# return redirect(redirect_url)
#
# return redirect('hr:performance_review_detail', pk=review.pk)
#
#
# class TrainingRecordListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
# """
# List view for training records.
# """
# model = TrainingRecord
# template_name = 'hr/training_record_list.html'
# context_object_name = 'training_records'
# paginate_by = 20
# permission_required = 'hr.view_trainingrecord'
#
# def get_queryset(self):
# """
# Filter training records by tenant and apply search filters.
# """
# queryset = TrainingRecord.objects.filter(tenant=self.request.user.tenant)
#
# # Apply search filters
# 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_number__icontains=search) |
# Q(training_title__icontains=search) |
# Q(training_description__icontains=search) |
# Q(provider__icontains=search) |
# Q(trainer__icontains=search)
# )
#
# employee_id = self.request.GET.get('employee', '')
# if employee_id:
# queryset = queryset.filter(employee_id=employee_id)
#
# training_type = self.request.GET.get('training_type', '')
# if training_type:
# queryset = queryset.filter(training_type=training_type)
#
# status = self.request.GET.get('status', '')
# if status:
# queryset = queryset.filter(status=status)
#
# is_mandatory = self.request.GET.get('is_mandatory', '')
# if is_mandatory == 'True':
# queryset = queryset.filter(is_mandatory=True)
# elif is_mandatory == 'False':
# queryset = queryset.filter(is_mandatory=False)
#
# # Date range filters
# start_from = self.request.GET.get('start_from', '')
# if start_from:
# try:
# start_from = timezone.datetime.strptime(start_from, '%Y-%m-%d').date()
# queryset = queryset.filter(start_date__gte=start_from)
# except (ValueError, TypeError):
# pass
#
# start_to = self.request.GET.get('start_to', '')
# if start_to:
# try:
# start_to = timezone.datetime.strptime(start_to, '%Y-%m-%d').date()
# queryset = queryset.filter(start_date__lte=start_to)
# except (ValueError, TypeError):
# pass
#
# # Expiry status filter
# expiry_status = self.request.GET.get('expiry_status', '')
# today = timezone.now().date()
# if expiry_status == 'valid':
# queryset = queryset.filter(
# status='COMPLETED',
# expiry_date__gt=today
# )
# elif expiry_status == 'expiring':
# queryset = queryset.filter(
# status='COMPLETED',
# expiry_date__gt=today,
# expiry_date__lte=today + timezone.timedelta(days=30)
# )
# elif expiry_status == 'expired':
# queryset = queryset.filter(
# Q(status='EXPIRED') |
# Q(status='COMPLETED', expiry_date__lt=today)
# )
#
# return queryset.select_related('employee')
#
# def get_context_data(self, **kwargs):
# """
# Add filter choices to context.
# """
# context = super().get_context_data(**kwargs)
# tenant = self.request.user.tenant
#
# context['search'] = self.request.GET.get('search', '')
# context['employees'] = Employee.objects.filter(
# tenant=tenant
# ).order_by('last_name', 'first_name')
# context['training_statuses'] = TrainingRecord.TRAINING_STATUS
# context['selected_employee'] = self.request.GET.get('employee', '')
# context['selected_type'] = self.request.GET.get('training_type', '')
# context['selected_status'] = self.request.GET.get('status', '')
# context['selected_mandatory'] = self.request.GET.get('is_mandatory', '')
# context['start_from'] = self.request.GET.get('start_from', '')
# context['start_to'] = self.request.GET.get('start_to', '')
# context['selected_expiry'] = self.request.GET.get('expiry_status', '')
#
# # Collect unique training types from existing records
# context['training_types'] = TrainingRecord.objects.filter(
# tenant=tenant
# ).values_list('training_type', flat=True).distinct().order_by('training_type')
#
# return context
#
#
# class TrainingRecordDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# Detail view for training records.
# """
# model = TrainingRecord
# template_name = 'hr/training_record_detail.html'
# context_object_name = 'training_record'
# permission_required = 'hr.view_trainingrecord'
#
# def get_queryset(self):
# """
# Filter training record by tenant.
# """
# return TrainingRecord.objects.filter(
# tenant=self.request.user.tenant
# ).select_related(
# 'employee', 'created_by'
# )
#
#
# class TrainingRecordCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
# """
# Create view for training records.
# """
# model = TrainingRecord
# form_class = TrainingRecordForm
# template_name = 'hr/training_record_form.html'
# permission_required = 'hr.add_trainingrecord'
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
#
# # Pre-select employee if provided in GET parameters
# employee_id = self.request.GET.get('employee', None)
# if employee_id:
# kwargs['initial'] = kwargs.get('initial', {})
# kwargs['initial']['employee'] = employee_id
#
# return kwargs
#
# def form_valid(self, form):
# """
# Set tenant and user before saving.
# """
# form.instance.tenant = self.request.user.tenant
# form.instance.created_by = self.request.user
#
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Training record '{form.instance.training_title}' for {form.instance.employee.get_full_name()} created successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to training record detail view.
# """
# return reverse('hr:training_record_detail', kwargs={'pk': self.object.pk})
#
#
# class TrainingRecordUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# """
# Update view for training records.
# """
# model = TrainingRecord
# form_class = TrainingRecordForm
# template_name = 'hr/training_record_form.html'
# permission_required = 'hr.change_trainingrecord'
#
# def get_queryset(self):
# """
# Filter training record by tenant.
# """
# return TrainingRecord.objects.filter(tenant=self.request.user.tenant)
#
# def get_form_kwargs(self):
# """
# Pass tenant and user to form.
# """
# kwargs = super().get_form_kwargs()
# kwargs['tenant'] = self.request.user.tenant
# kwargs['user'] = self.request.user
# return kwargs
#
# def form_valid(self, form):
# """
# Handle successful form validation.
# """
# response = super().form_valid(form)
#
# messages.success(
# self.request,
# f"Training record '{form.instance.training_title}' updated successfully."
# )
#
# return response
#
# def get_success_url(self):
# """
# Redirect to training record detail view.
# """
# return reverse('hr:training_record_detail', kwargs={'pk': self.object.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(
# tenant=request.user.tenant,
# status='PENDING'
# ).count(),
# 'employees_clocked_in': TimeEntry.objects.filter(
# 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 clock_in(request):
# """
# Clock in the current user.
# """
# # Check if user has an employee profile
# try:
# employee = request.user.employee_profile
# except (AttributeError, Employee.DoesNotExist):
# messages.error(request, "You don't have an employee profile. Please contact HR.")
# return redirect('hr:dashboard')
#
# # Check if already clocked in
# existing_entry = TimeEntry.objects.filter(
# tenant=request.user.tenant,
# employee=employee,
# entry_date=timezone.now().date(),
# clock_out_time__isnull=True
# ).first()
#
# if existing_entry:
# messages.warning(request, "You are already clocked in. Please clock out first.")
# return redirect('hr:dashboard')
#
# # Create new time entry
# time_entry = TimeEntry(
# tenant=request.user.tenant,
# employee=employee,
# entry_date=timezone.now().date(),
# entry_type='REGULAR',
# clock_in_time=timezone.now(),
# department=employee.department,
# created_by=request.user
# )
# time_entry.save()
#
# messages.success(request, f"Clocked in at {time_entry.clock_in_time.strftime('%H:%M:%S')}.")
# return redirect('hr:dashboard')
#
#
# @login_required
# def clock_out(request):
# """
# Clock out the current user.
# """
# # Check if user has an employee profile
# try:
# employee = request.user.employee_profile
# except (AttributeError, Employee.DoesNotExist):
# messages.error(request, "You don't have an employee profile. Please contact HR.")
# return redirect('hr:dashboard')
#
# # Find active time entry
# time_entry = TimeEntry.objects.filter(
# tenant=request.user.tenant,
# employee=employee,
# entry_date=timezone.now().date(),
# clock_out_time__isnull=True
# ).order_by('-clock_in_time').first()
#
# if not time_entry:
# messages.warning(request, "You are not clocked in. Please clock in first.")
# return redirect('hr:dashboard')
#
# # Update time entry
# time_entry.clock_out_time = timezone.now()
# time_entry.save() # This will calculate total_hours in the save method
#
# hours = time_entry.total_hours
# messages.success(
# request,
# f"Clocked out at {time_entry.clock_out_time.strftime('%H:%M:%S')}. "
# f"Total hours: {hours:.2f}"
# )
# return redirect('hr:dashboard')
#
#
# @login_required
# def employee_schedule(request, employee_id):
# """
# View an employee's schedule.
# """
# employee = get_object_or_404(
# Employee,
# pk=employee_id,
# tenant=request.user.tenant
# )
#
# # Get current schedule
# current_schedule = Schedule.objects.filter(
# employee=employee,
# is_current=True,
# is_active=True
# ).first()
#
# # Get upcoming assignments
# today = timezone.now().date()
# upcoming_assignments = ScheduleAssignment.objects.filter(
# schedule__employee=employee,
# assignment_date__gte=today
# ).order_by('assignment_date', 'start_time')[:14] # Next 2 weeks
#
# context = {
# 'employee': employee,
# 'current_schedule': current_schedule,
# 'upcoming_assignments': upcoming_assignments,
# }
#
# return render(request, 'hr/employee_schedule.html', context)
#