554 lines
25 KiB
HTML
554 lines
25 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Employee Management - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="fas fa-users me-2"></i>Employee Management
|
|
</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
|
<li class="breadcrumb-item active">Employees</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-download me-2"></i>Export
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="exportEmployees('csv')">
|
|
<i class="fas fa-file-csv me-2"></i>Export as CSV
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportEmployees('excel')">
|
|
<i class="fas fa-file-excel me-2"></i>Export as Excel
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportEmployees('pdf')">
|
|
<i class="fas fa-file-pdf me-2"></i>Export as PDF
|
|
</a></li>
|
|
</ul>
|
|
<a href="{% url 'hr:employee_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Add Employee
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">Total Employees</h6>
|
|
<h3 class="mb-0 text-primary">{{ total_employees }}</h3>
|
|
<small class="text-muted">All staff members</small>
|
|
</div>
|
|
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-users fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">Active Employees</h6>
|
|
<h3 class="mb-0 text-success">{{ active_employees }}</h3>
|
|
<small class="text-muted">Currently employed</small>
|
|
</div>
|
|
<div class="bg-success bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-user-check fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">New Hires</h6>
|
|
<h3 class="mb-0 text-info">{{ new_hires_count }}</h3>
|
|
<small class="text-muted">This month</small>
|
|
</div>
|
|
<div class="bg-info bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-user-plus fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title text-muted mb-1">Departments</h6>
|
|
<h3 class="mb-0 text-warning">{{ departments_count }}</h3>
|
|
<small class="text-muted">Active departments</small>
|
|
</div>
|
|
<div class="bg-warning bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
<i class="fas fa-building fa-lg text-white"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters Card -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-filter me-2"></i>Filters
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3">
|
|
<div class="col-md-2">
|
|
<label for="department" class="form-label">Department</label>
|
|
<select name="department" id="department" class="form-select">
|
|
<option value="">All Departments</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}" {% if request.GET.department == dept.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ dept.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="employment_status" class="form-label">Status</label>
|
|
<select name="employment_status" id="employment_status" class="form-select">
|
|
<option value="">All Status</option>
|
|
<option value="ACTIVE" {% if request.GET.employment_status == "ACTIVE" %}selected{% endif %}>Active</option>
|
|
<option value="INACTIVE" {% if request.GET.employment_status == "INACTIVE" %}selected{% endif %}>Inactive</option>
|
|
<option value="TERMINATED" {% if request.GET.employment_status == "TERMINATED" %}selected{% endif %}>Terminated</option>
|
|
<option value="ON_LEAVE" {% if request.GET.employment_status == "ON_LEAVE" %}selected{% endif %}>On Leave</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="job_title" class="form-label">Job Title</label>
|
|
<input type="text" name="job_title" id="job_title" class="form-control"
|
|
placeholder="Job title..." value="{{ request.GET.job_title }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="hire_date_from" class="form-label">Hired From</label>
|
|
<input type="date" name="hire_date_from" id="hire_date_from" class="form-control"
|
|
value="{{ request.GET.hire_date_from }}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="search" class="form-label">Search</label>
|
|
<input type="text" name="search" id="search" class="form-control"
|
|
placeholder="Search employees..." value="{{ request.GET.search }}">
|
|
</div>
|
|
<div class="col-md-1 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary me-2">
|
|
<i class="fas fa-search me-1"></i>Filter
|
|
</button>
|
|
<a href="{% url 'hr:employee_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-times"></i>
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Employees List -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-list me-2"></i>Employees
|
|
{% if employees %}
|
|
<span class="badge bg-secondary ms-2">{{ employees|length }}</span>
|
|
{% endif %}
|
|
</h5>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="refreshEmployees()">
|
|
<i class="fas fa-sync-alt me-1"></i>Refresh
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="toggleBulkActions()">
|
|
<i class="fas fa-check-square me-1"></i>Bulk Actions
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body p-0">
|
|
<!-- Bulk Actions Bar (Hidden by default) -->
|
|
<div id="bulk-actions-bar" class="bg-light border-bottom p-3" style="display: none;">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<span id="selected-count">0</span> employee(s) selected
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary" onclick="bulkUpdateStatus()">
|
|
<i class="fas fa-edit me-1"></i>Update Status
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success" onclick="bulkExport()">
|
|
<i class="fas fa-download me-1"></i>Export Selected
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="clearSelection()">
|
|
<i class="fas fa-times me-1"></i>Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th width="40">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="select-all">
|
|
</div>
|
|
</th>
|
|
<th>Employee</th>
|
|
<th>Employee ID</th>
|
|
<th>Department</th>
|
|
<th>Job Title</th>
|
|
<th>Hire Date</th>
|
|
<th>Status</th>
|
|
<th width="120">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for employee in employees %}
|
|
<tr>
|
|
<td>
|
|
<div class="form-check">
|
|
<input class="form-check-input employee-checkbox" type="checkbox"
|
|
value="{{ employee.id }}">
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar-sm bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center me-2">
|
|
<span class="text-white small fw-bold">
|
|
{{ employee.first_name.0 }}{{ employee.last_name.0 }}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">{{ employee.get_full_name }}</div>
|
|
<small class="text-muted">{{ employee.email|default:"No email" }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="font-monospace">{{ employee.employee_number }}</span>
|
|
</td>
|
|
<td>
|
|
{% if employee.department %}
|
|
<span class="badge bg-info">{{ employee.department.name }}</span>
|
|
{% else %}
|
|
<span class="text-muted">Not assigned</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ employee.job_title|default:"Not specified" }}</td>
|
|
<td>
|
|
<div>{{ employee.hire_date|date:"M d, Y" }}</div>
|
|
<small class="text-muted">{{ employee.hire_date|timesince }} ago</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{% if employee.employment_status == 'ACTIVE' %}success{% elif employee.employment_status == 'INACTIVE' %}secondary{% elif employee.employment_status == 'TERMINATED' %}danger{% elif employee.employment_status == 'ON_LEAVE' %}warning{% else %}secondary{% endif %}">
|
|
{{ employee.get_employment_status_display }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{% url 'hr:employee_detail' employee.pk %}"
|
|
class="btn btn-outline-primary btn-sm" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle dropdown-toggle-split"
|
|
data-bs-toggle="dropdown">
|
|
<span class="visually-hidden">Toggle Dropdown</span>
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="{% url 'hr:employee_update' employee.pk %}">
|
|
<i class="fas fa-edit me-2"></i>Edit Employee
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#">
|
|
<i class="fas fa-clock me-2"></i>View Schedule
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#">
|
|
<i class="fas fa-chart-line me-2"></i>Performance Review
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#">
|
|
<i class="fas fa-graduation-cap me-2"></i>Training Records
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item" href="#">
|
|
<i class="fas fa-print me-2"></i>Print Profile
|
|
</a></li>
|
|
<li><a class="dropdown-item text-danger" href="{% url 'hr:employee_delete' employee.pk %}">
|
|
<i class="fas fa-trash me-2"></i>Delete
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="8" class="text-center py-5">
|
|
<i class="fas fa-users fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No employees found</h5>
|
|
<p class="text-muted">No employees match your current filters.</p>
|
|
<a href="{% url 'hr:employee_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Add First Employee
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<div class="card-footer">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="text-muted small">
|
|
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} employees
|
|
</div>
|
|
<nav aria-label="Employees pagination">
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ page_obj.number }}</span>
|
|
</li>
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Employee list functionality
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize bulk selection
|
|
const selectAllCheckbox = document.getElementById('select-all');
|
|
const employeeCheckboxes = document.querySelectorAll('.employee-checkbox');
|
|
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.addEventListener('change', function() {
|
|
employeeCheckboxes.forEach(checkbox => {
|
|
checkbox.checked = this.checked;
|
|
});
|
|
updateSelectedCount();
|
|
});
|
|
}
|
|
|
|
employeeCheckboxes.forEach(checkbox => {
|
|
checkbox.addEventListener('change', function() {
|
|
updateSelectedCount();
|
|
|
|
// Update select all checkbox state
|
|
const checkedCount = document.querySelectorAll('.employee-checkbox:checked').length;
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.checked = checkedCount === employeeCheckboxes.length;
|
|
selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < employeeCheckboxes.length;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
function updateSelectedCount() {
|
|
const checkedCount = document.querySelectorAll('.employee-checkbox:checked').length;
|
|
const selectedCountElement = document.getElementById('selected-count');
|
|
|
|
if (selectedCountElement) {
|
|
selectedCountElement.textContent = checkedCount;
|
|
}
|
|
|
|
// Show/hide bulk actions bar
|
|
const bulkActionsBar = document.getElementById('bulk-actions-bar');
|
|
if (bulkActionsBar) {
|
|
bulkActionsBar.style.display = checkedCount > 0 ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
function toggleBulkActions() {
|
|
const bulkActionsBar = document.getElementById('bulk-actions-bar');
|
|
const isVisible = bulkActionsBar.style.display !== 'none';
|
|
|
|
if (isVisible) {
|
|
clearSelection();
|
|
} else {
|
|
bulkActionsBar.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function clearSelection() {
|
|
document.querySelectorAll('.employee-checkbox').forEach(checkbox => {
|
|
checkbox.checked = false;
|
|
});
|
|
|
|
const selectAllCheckbox = document.getElementById('select-all');
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.checked = false;
|
|
selectAllCheckbox.indeterminate = false;
|
|
}
|
|
|
|
updateSelectedCount();
|
|
}
|
|
|
|
function refreshEmployees() {
|
|
location.reload();
|
|
}
|
|
|
|
function exportEmployees(format) {
|
|
const form = document.querySelector('form');
|
|
const formData = new FormData(form);
|
|
const params = new URLSearchParams(formData);
|
|
params.append('format', format);
|
|
|
|
window.open(`{% url 'hr:employee_list' %}export/?${params}`, '_blank');
|
|
}
|
|
|
|
function bulkUpdateStatus() {
|
|
const selectedIds = Array.from(document.querySelectorAll('.employee-checkbox:checked'))
|
|
.map(checkbox => checkbox.value);
|
|
|
|
if (selectedIds.length === 0) {
|
|
alert('Please select employees to update');
|
|
return;
|
|
}
|
|
|
|
const newStatus = prompt('Enter new status (ACTIVE, INACTIVE, TERMINATED, ON_LEAVE):');
|
|
if (newStatus) {
|
|
// Implementation for bulk status update
|
|
console.log('Updating employees:', selectedIds, 'to status:', newStatus);
|
|
alert('Bulk status update functionality would be implemented here');
|
|
}
|
|
}
|
|
|
|
function bulkExport() {
|
|
const selectedIds = Array.from(document.querySelectorAll('.employee-checkbox:checked'))
|
|
.map(checkbox => checkbox.value);
|
|
|
|
if (selectedIds.length === 0) {
|
|
alert('Please select employees to export');
|
|
return;
|
|
}
|
|
|
|
const params = new URLSearchParams();
|
|
selectedIds.forEach(id => params.append('ids', id));
|
|
|
|
window.open(`{% url 'hr:employee_list' %}export/?${params}`, '_blank');
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') {
|
|
return;
|
|
}
|
|
|
|
switch (e.key) {
|
|
case 'n':
|
|
e.preventDefault();
|
|
window.location.href = "{% url 'hr:employee_create' %}";
|
|
break;
|
|
case 'r':
|
|
e.preventDefault();
|
|
refreshEmployees();
|
|
break;
|
|
case '/':
|
|
e.preventDefault();
|
|
document.getElementById('search').focus();
|
|
break;
|
|
case 'a':
|
|
if (e.ctrlKey || e.metaKey) {
|
|
e.preventDefault();
|
|
const selectAllCheckbox = document.getElementById('select-all');
|
|
if (selectAllCheckbox) {
|
|
selectAllCheckbox.click();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.avatar-sm {
|
|
width: 32px;
|
|
height: 32px;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.bg-gradient {
|
|
background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
|
}
|
|
|
|
.table-hover tbody tr:hover {
|
|
background-color: rgba(0, 0, 0, 0.02);
|
|
}
|
|
|
|
.font-monospace {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.form-check-input:indeterminate {
|
|
background-color: #0d6efd;
|
|
border-color: #0d6efd;
|
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
|
|
}
|
|
|
|
#bulk-actions-bar {
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.btn-group-sm .btn {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.d-flex.justify-content-between {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|