367 lines
11 KiB
HTML
367 lines
11 KiB
HTML
{% load static %}
|
|
|
|
<div class="department-tree">
|
|
{% for department in departments %}
|
|
{% include 'hr/departments/department_tree_node.html' with department=department level=0 %}
|
|
{% empty %}
|
|
<div class="text-center text-muted py-4">
|
|
<i class="fas fa-building fa-3x mb-3"></i>
|
|
<p class="mb-0">No departments found</p>
|
|
<small>Create your first department to get started</small>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Department tree node template -->
|
|
{% verbatim %}
|
|
<script type="text/template" id="department-node-template">
|
|
<div class="department-node" data-department-id="{{id}}">
|
|
<div class="department-item d-flex align-items-center py-2 px-3 border-bottom">
|
|
<div class="department-toggle me-2" style="width: 20px;">
|
|
{{#if hasChildren}}
|
|
<i class="fas fa-chevron-right toggle-icon" data-bs-toggle="collapse"
|
|
data-bs-target="#dept-{{id}}-children" aria-expanded="false"></i>
|
|
{{/if}}
|
|
</div>
|
|
|
|
<div class="department-icon me-3">
|
|
<i class="fas {{icon}} text-{{color}}"></i>
|
|
</div>
|
|
|
|
<div class="department-info flex-grow-1">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h6 class="mb-0 department-name">
|
|
<a href="/hr/departments/{{id}}/"
|
|
class="text-decoration-none">{{name}}</a>
|
|
{{#unless is_active}}
|
|
<span class="badge badge-secondary ms-2">Inactive</span>
|
|
{{/unless}}
|
|
</h6>
|
|
<small class="text-muted">
|
|
{{department_type}} • {{employee_count}} employees
|
|
{{#if department_head}}
|
|
• Head: {{department_head}}
|
|
{{/if}}
|
|
</small>
|
|
</div>
|
|
|
|
<div class="department-actions">
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<a href="/hr/departments/{{id}}/"
|
|
class="btn btn-outline-primary btn-sm" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<a href="/hr/departments/{{id}}/edit/"
|
|
class="btn btn-outline-secondary btn-sm" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
{{#if is_active}}
|
|
<button class="btn btn-outline-warning btn-sm"
|
|
onclick="deactivateDepartment('{{id}}')" title="Deactivate">
|
|
<i class="fas fa-pause"></i>
|
|
</button>
|
|
{{else}}
|
|
<button class="btn btn-outline-success btn-sm"
|
|
onclick="activateDepartment('{{id}}')" title="Activate">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
{{/if}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{#if hasChildren}}
|
|
<div class="collapse" id="dept-{{id}}-children">
|
|
<div class="department-children ps-4">
|
|
<!-- Children will be loaded here -->
|
|
</div>
|
|
</div>
|
|
{{/if}}
|
|
</div>
|
|
</script>
|
|
{% endverbatim %}
|
|
|
|
<style>
|
|
.department-tree {
|
|
background: #fff;
|
|
border-radius: 0.375rem;
|
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
|
}
|
|
|
|
.department-node {
|
|
border-left: 3px solid transparent;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.department-node:hover {
|
|
background-color: #f8f9fa;
|
|
border-left-color: #007bff;
|
|
}
|
|
|
|
.department-node.active {
|
|
background-color: #e3f2fd;
|
|
border-left-color: #2196f3;
|
|
}
|
|
|
|
.department-item {
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.department-toggle {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.toggle-icon {
|
|
transition: transform 0.2s ease;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.toggle-icon:hover {
|
|
color: #495057;
|
|
}
|
|
|
|
.toggle-icon.expanded {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.department-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: #f8f9fa;
|
|
border-radius: 50%;
|
|
border: 2px solid #e9ecef;
|
|
}
|
|
|
|
.department-info .department-name a {
|
|
color: #212529;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.department-info .department-name a:hover {
|
|
color: #007bff;
|
|
}
|
|
|
|
.department-actions {
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.department-node:hover .department-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.department-children {
|
|
border-left: 1px dashed #dee2e6;
|
|
margin-left: 20px;
|
|
}
|
|
|
|
.department-children .department-node {
|
|
border-left-width: 2px;
|
|
}
|
|
|
|
.department-children .department-children .department-node {
|
|
border-left-width: 1px;
|
|
}
|
|
|
|
/* Department type colors */
|
|
.department-clinical { color: #dc3545; }
|
|
.department-administrative { color: #6f42c1; }
|
|
.department-support { color: #fd7e14; }
|
|
.department-ancillary { color: #20c997; }
|
|
.department-executive { color: #0d6efd; }
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 768px) {
|
|
.department-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.department-actions .btn-group {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.department-info small {
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
|
|
/* Loading states */
|
|
.department-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.department-loading .spinner-border {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
/* Empty state */
|
|
.department-tree-empty {
|
|
text-align: center;
|
|
padding: 3rem 1rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.department-tree-empty .fa-building {
|
|
color: #dee2e6;
|
|
margin-bottom: 1rem;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize department tree functionality
|
|
initializeDepartmentTree();
|
|
});
|
|
|
|
function initializeDepartmentTree() {
|
|
// Handle toggle clicks
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.classList.contains('toggle-icon')) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const icon = e.target;
|
|
const targetId = icon.getAttribute('data-bs-target');
|
|
const target = document.querySelector(targetId);
|
|
|
|
if (target) {
|
|
// Toggle icon rotation
|
|
icon.classList.toggle('expanded');
|
|
|
|
// Load children if not already loaded
|
|
if (!target.hasAttribute('data-loaded')) {
|
|
loadDepartmentChildren(target, icon.getAttribute('data-department-id'));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle department node clicks
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.closest('.department-item') && !e.target.closest('.department-actions')) {
|
|
const departmentItem = e.target.closest('.department-item');
|
|
const departmentNode = departmentItem.closest('.department-node');
|
|
|
|
// Remove active class from all nodes
|
|
document.querySelectorAll('.department-node.active').forEach(node => {
|
|
node.classList.remove('active');
|
|
});
|
|
|
|
// Add active class to clicked node
|
|
departmentNode.classList.add('active');
|
|
|
|
// Emit custom event for other components
|
|
const departmentId = departmentNode.getAttribute('data-department-id');
|
|
document.dispatchEvent(new CustomEvent('departmentSelected', {
|
|
detail: { departmentId: departmentId }
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadDepartmentChildren(container, departmentId) {
|
|
// Show loading state
|
|
container.innerHTML = '<div class="department-loading"><div class="spinner-border spinner-border-sm" role="status"></div>Loading...</div>';
|
|
|
|
// Make HTMX request to load children
|
|
htmx.ajax('GET', `/hr/api/departments/${departmentId}/children/`, {
|
|
target: container,
|
|
swap: 'innerHTML'
|
|
}).then(() => {
|
|
container.setAttribute('data-loaded', 'true');
|
|
}).catch(() => {
|
|
container.innerHTML = '<div class="text-danger p-2"><i class="fas fa-exclamation-triangle"></i> Failed to load children</div>';
|
|
});
|
|
}
|
|
|
|
function activateDepartment(departmentId) {
|
|
if (confirm('Are you sure you want to activate this department?')) {
|
|
htmx.ajax('POST', `/hr/departments/${departmentId}/activate/`, {
|
|
headers: {
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
}).then(() => {
|
|
// Refresh the tree
|
|
refreshDepartmentTree();
|
|
showToast('Department activated successfully', 'success');
|
|
}).catch(() => {
|
|
showToast('Failed to activate department', 'error');
|
|
});
|
|
}
|
|
}
|
|
|
|
function deactivateDepartment(departmentId) {
|
|
if (confirm('Are you sure you want to deactivate this department? This will affect all associated employees and schedules.')) {
|
|
htmx.ajax('POST', `/hr/departments/${departmentId}/deactivate/`, {
|
|
headers: {
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
}).then(() => {
|
|
// Refresh the tree
|
|
refreshDepartmentTree();
|
|
showToast('Department deactivated successfully', 'warning');
|
|
}).catch(() => {
|
|
showToast('Failed to deactivate department', 'error');
|
|
});
|
|
}
|
|
}
|
|
|
|
function refreshDepartmentTree() {
|
|
htmx.ajax('GET', '/hr/departments/tree/', {
|
|
target: '.department-tree',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
|
|
function expandAllDepartments() {
|
|
document.querySelectorAll('.toggle-icon').forEach(icon => {
|
|
if (!icon.classList.contains('expanded')) {
|
|
icon.click();
|
|
}
|
|
});
|
|
}
|
|
|
|
function collapseAllDepartments() {
|
|
document.querySelectorAll('.toggle-icon.expanded').forEach(icon => {
|
|
icon.click();
|
|
});
|
|
}
|
|
|
|
function getCsrfToken() {
|
|
return document.querySelector('[name=csrfmiddlewaretoken]').value;
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
// Use your existing toast notification system
|
|
if (typeof HospitalApp !== 'undefined' && HospitalApp.utils && HospitalApp.utils.showToast) {
|
|
HospitalApp.utils.showToast(message, type);
|
|
} else {
|
|
// Fallback to basic alert
|
|
alert(message);
|
|
}
|
|
}
|
|
|
|
// Export functions for external use
|
|
window.DepartmentTree = {
|
|
refresh: refreshDepartmentTree,
|
|
expandAll: expandAllDepartments,
|
|
collapseAll: collapseAllDepartments,
|
|
activate: activateDepartment,
|
|
deactivate: deactivateDepartment
|
|
};
|
|
</script>
|