1204 lines
61 KiB
HTML
1204 lines
61 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Training Management{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<div class="container">
|
|
<ul class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
|
<li class="breadcrumb-item active">Training Management</li>
|
|
</ul>
|
|
|
|
<div class="row align-items-center mb-3">
|
|
<div class="col">
|
|
<h1 class="page-header">Training Management</h1>
|
|
<p class="text-muted">Employee training programs and certification tracking</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="btn-group">
|
|
<button class="btn btn-primary" onclick="createTraining()">
|
|
<i class="fa fa-plus me-2"></i>New Training
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="createProgram()">
|
|
<i class="fa fa-graduation-cap me-2"></i>New Program
|
|
</button>
|
|
<button class="btn btn-outline-info" onclick="viewCalendar()">
|
|
<i class="fa fa-calendar me-2"></i>Calendar
|
|
</button>
|
|
<button class="btn btn-outline-success" onclick="generateReport()">
|
|
<i class="fa fa-chart-bar me-2"></i>Reports
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Training Overview -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card bg-primary text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h4 class="mb-0">{{ total_programs }}</h4>
|
|
<p class="mb-0">Training Programs</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-graduation-cap fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-success text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h4 class="mb-0">{{ completed_trainings }}</h4>
|
|
<p class="mb-0">Completed</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-check-circle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-warning text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h4 class="mb-0">{{ pending_trainings }}</h4>
|
|
<p class="mb-0">In Progress</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-clock fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-danger text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h4 class="mb-0">{{ overdue_trainings }}</h4>
|
|
<p class="mb-0">Overdue</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-exclamation-triangle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Training Tabs -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs" id="trainingTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="programs-tab" data-bs-toggle="tab" data-bs-target="#programs" type="button" role="tab">
|
|
<i class="fa fa-graduation-cap me-2"></i>Training Programs
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="sessions-tab" data-bs-toggle="tab" data-bs-target="#sessions" type="button" role="tab">
|
|
<i class="fa fa-calendar-alt me-2"></i>Training Sessions
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="certifications-tab" data-bs-toggle="tab" data-bs-target="#certifications" type="button" role="tab">
|
|
<i class="fa fa-certificate me-2"></i>Certifications
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance" type="button" role="tab">
|
|
<i class="fa fa-shield-alt me-2"></i>Compliance
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="tab-content" id="trainingTabContent">
|
|
<!-- Training Programs Tab -->
|
|
<div class="tab-pane fade show active" id="programs" role="tabpanel">
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Search programs..." id="programSearch">
|
|
<button class="btn btn-outline-secondary" type="button">
|
|
<i class="fa fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select class="form-select" id="programCategory">
|
|
<option value="">All Categories</option>
|
|
<option value="mandatory">Mandatory</option>
|
|
<option value="clinical">Clinical</option>
|
|
<option value="safety">Safety</option>
|
|
<option value="compliance">Compliance</option>
|
|
<option value="professional">Professional Development</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select class="form-select" id="programStatus">
|
|
<option value="">All Status</option>
|
|
<option value="active">Active</option>
|
|
<option value="draft">Draft</option>
|
|
<option value="archived">Archived</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
{% for program in training_programs %}
|
|
<div class="col-md-6 col-lg-4 mb-4">
|
|
<div class="card h-100 training-program-card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="card-title mb-0">{{ program.title }}</h6>
|
|
<span class="badge bg-{{ program.status_color }}">{{ program.get_status_display }}</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="card-text text-muted">{{ program.description|truncatewords:20 }}</p>
|
|
<div class="mb-2">
|
|
<small class="text-muted">
|
|
<i class="fa fa-tag me-1"></i>{{ program.get_category_display }}
|
|
</small>
|
|
</div>
|
|
<div class="mb-2">
|
|
<small class="text-muted">
|
|
<i class="fa fa-clock me-1"></i>{{ program.duration_hours }}h duration
|
|
</small>
|
|
</div>
|
|
<div class="mb-2">
|
|
<small class="text-muted">
|
|
<i class="fa fa-users me-1"></i>{{ program.enrolled_count }} enrolled
|
|
</small>
|
|
</div>
|
|
{% if program.is_mandatory %}
|
|
<div class="mb-2">
|
|
<span class="badge bg-danger">Mandatory</span>
|
|
</div>
|
|
{% endif %}
|
|
<div class="progress mb-2" style="height: 6px;">
|
|
<div class="progress-bar bg-success"
|
|
role="progressbar"
|
|
style="width: {{ program.completion_rate }}%"
|
|
title="{{ program.completion_rate }}% completion rate">
|
|
</div>
|
|
</div>
|
|
<small class="text-muted">{{ program.completion_rate }}% completion rate</small>
|
|
</div>
|
|
<div class="card-footer">
|
|
<div class="btn-group w-100">
|
|
<button class="btn btn-outline-primary btn-sm" onclick="viewProgram('{{ program.id }}')">
|
|
<i class="fa fa-eye"></i> View
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="editProgram('{{ program.id }}')">
|
|
<i class="fa fa-edit"></i> Edit
|
|
</button>
|
|
<button class="btn btn-outline-success btn-sm" onclick="scheduleSession('{{ program.id }}')">
|
|
<i class="fa fa-calendar-plus"></i> Schedule
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="col-12">
|
|
<div class="text-center py-4">
|
|
<i class="fa fa-graduation-cap fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No training programs found</h5>
|
|
<p class="text-muted">Create your first training program to get started.</p>
|
|
<button class="btn btn-primary" onclick="createProgram()">
|
|
<i class="fa fa-plus me-2"></i>Create Program
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Training Sessions Tab -->
|
|
<div class="tab-pane fade" id="sessions" role="tabpanel">
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Search sessions..." id="sessionSearch">
|
|
<button class="btn btn-outline-secondary" type="button">
|
|
<i class="fa fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select class="form-select" id="sessionStatus">
|
|
<option value="">All Status</option>
|
|
<option value="scheduled">Scheduled</option>
|
|
<option value="in_progress">In Progress</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<input type="date" class="form-control" id="sessionDate" placeholder="Date">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select class="form-select" id="sessionInstructor">
|
|
<option value="">All Instructors</option>
|
|
{% for instructor in instructors %}
|
|
<option value="{{ instructor.id }}">{{ instructor.get_full_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-primary w-100" onclick="scheduleNewSession()">
|
|
<i class="fa fa-plus me-1"></i>Schedule
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Session</th>
|
|
<th>Program</th>
|
|
<th>Date & Time</th>
|
|
<th>Instructor</th>
|
|
<th>Attendees</th>
|
|
<th>Location</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for session in training_sessions %}
|
|
<tr>
|
|
<td>
|
|
<div>
|
|
<h6 class="mb-0">{{ session.title }}</h6>
|
|
<small class="text-muted">{{ session.session_code }}</small>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">{{ session.program.title }}</span>
|
|
</td>
|
|
<td>
|
|
<div>
|
|
<strong>{{ session.start_datetime|date:"M d, Y" }}</strong>
|
|
<br><small class="text-muted">{{ session.start_datetime|time:"H:i" }} - {{ session.end_datetime|time:"H:i" }}</small>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if session.instructor %}
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar avatar-sm me-2">
|
|
<div class="avatar-initial rounded-circle bg-primary">
|
|
{{ session.instructor.first_name|first }}{{ session.instructor.last_name|first }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<small>{{ session.instructor.get_full_name }}</small>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">Not assigned</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<span class="me-2">{{ session.attendee_count }}/{{ session.max_attendees }}</span>
|
|
<div class="progress flex-grow-1" style="height: 6px;">
|
|
<div class="progress-bar bg-info"
|
|
role="progressbar"
|
|
style="width: {{ session.attendance_percentage }}%">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<small>{{ session.location|default:"TBD" }}</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{{ session.status_color }}">
|
|
{{ session.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="viewSession('{{ session.id }}')" title="View Details">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="editSession('{{ session.id }}')" title="Edit">
|
|
<i class="fa fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-outline-info" onclick="manageAttendees('{{ session.id }}')" title="Manage Attendees">
|
|
<i class="fa fa-users"></i>
|
|
</button>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-success dropdown-toggle" data-bs-toggle="dropdown" title="More Actions">
|
|
<i class="fa fa-ellipsis-v"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
{% if session.status == 'scheduled' %}
|
|
<li><a class="dropdown-item" href="#" onclick="startSession('{{ session.id }}')">
|
|
<i class="fa fa-play me-2"></i>Start Session
|
|
</a></li>
|
|
{% elif session.status == 'in_progress' %}
|
|
<li><a class="dropdown-item" href="#" onclick="completeSession('{{ session.id }}')">
|
|
<i class="fa fa-check me-2"></i>Complete Session
|
|
</a></li>
|
|
{% endif %}
|
|
<li><a class="dropdown-item" href="#" onclick="duplicateSession('{{ session.id }}')">
|
|
<i class="fa fa-copy me-2"></i>Duplicate
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="sendReminders('{{ session.id }}')">
|
|
<i class="fa fa-bell me-2"></i>Send Reminders
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="#" onclick="cancelSession('{{ session.id }}')">
|
|
<i class="fa fa-times me-2"></i>Cancel Session
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="8" class="text-center py-4">
|
|
<i class="fa fa-calendar-alt fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No training sessions scheduled</h5>
|
|
<p class="text-muted">Schedule your first training session.</p>
|
|
<button class="btn btn-primary" onclick="scheduleNewSession()">
|
|
<i class="fa fa-plus me-2"></i>Schedule Session
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Certifications Tab -->
|
|
<div class="tab-pane fade" id="certifications" role="tabpanel">
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Search certifications..." id="certSearch">
|
|
<button class="btn btn-outline-secondary" type="button">
|
|
<i class="fa fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select class="form-select" id="certStatus">
|
|
<option value="">All Status</option>
|
|
<option value="active">Active</option>
|
|
<option value="expired">Expired</option>
|
|
<option value="expiring_soon">Expiring Soon</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select class="form-select" id="certType">
|
|
<option value="">All Types</option>
|
|
<option value="clinical">Clinical</option>
|
|
<option value="safety">Safety</option>
|
|
<option value="compliance">Compliance</option>
|
|
<option value="professional">Professional</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select class="form-select" id="certDepartment">
|
|
<option value="">All Departments</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}">{{ dept.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-primary w-100" onclick="addCertification()">
|
|
<i class="fa fa-plus me-1"></i>Add Cert
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Employee</th>
|
|
<th>Certification</th>
|
|
<th>Type</th>
|
|
<th>Issued Date</th>
|
|
<th>Expiry Date</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for cert in certifications %}
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar avatar-sm me-2">
|
|
{% if cert.employee.photo %}
|
|
<img src="{{ cert.employee.photo.url }}" alt="{{ cert.employee.get_full_name }}" class="rounded-circle">
|
|
{% else %}
|
|
<div class="avatar-initial rounded-circle bg-primary">
|
|
{{ cert.employee.first_name|first }}{{ cert.employee.last_name|first }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<h6 class="mb-0">{{ cert.employee.get_full_name }}</h6>
|
|
<small class="text-muted">{{ cert.employee.job_title }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div>
|
|
<strong>{{ cert.certification_name }}</strong>
|
|
<br><small class="text-muted">{{ cert.issuing_organization }}</small>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">{{ cert.get_certification_type_display }}</span>
|
|
</td>
|
|
<td>{{ cert.issue_date|date:"M d, Y" }}</td>
|
|
<td>
|
|
{% if cert.expiry_date %}
|
|
<span class="{% if cert.is_expired %}text-danger{% elif cert.is_expiring_soon %}text-warning{% endif %}">
|
|
{{ cert.expiry_date|date:"M d, Y" }}
|
|
</span>
|
|
{% else %}
|
|
<span class="text-muted">No expiry</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{{ cert.status_color }}">
|
|
{{ cert.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="viewCertification('{{ cert.id }}')" title="View Details">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="editCertification('{{ cert.id }}')" title="Edit">
|
|
<i class="fa fa-edit"></i>
|
|
</button>
|
|
{% if cert.is_expiring_soon %}
|
|
<button class="btn btn-outline-warning" onclick="renewCertification('{{ cert.id }}')" title="Renew">
|
|
<i class="fa fa-refresh"></i>
|
|
</button>
|
|
{% endif %}
|
|
<button class="btn btn-outline-info" onclick="downloadCertificate('{{ cert.id }}')" title="Download">
|
|
<i class="fa fa-download"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="7" class="text-center py-4">
|
|
<i class="fa fa-certificate fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No certifications found</h5>
|
|
<p class="text-muted">Add employee certifications to track compliance.</p>
|
|
<button class="btn btn-primary" onclick="addCertification()">
|
|
<i class="fa fa-plus me-2"></i>Add Certification
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Compliance Tab -->
|
|
<div class="tab-pane fade" id="compliance" role="tabpanel">
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">Compliance Overview</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h2 text-success">{{ compliance_rate }}%</div>
|
|
<div class="text-muted">Overall Compliance</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<div class="h2 text-warning">{{ expiring_soon_count }}</div>
|
|
<div class="text-muted">Expiring Soon</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">Department Compliance</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for dept in department_compliance %}
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span>{{ dept.name }}</span>
|
|
<div class="d-flex align-items-center">
|
|
<div class="progress me-2" style="width: 100px; height: 8px;">
|
|
<div class="progress-bar bg-{{ dept.compliance_color }}"
|
|
role="progressbar"
|
|
style="width: {{ dept.compliance_rate }}%">
|
|
</div>
|
|
</div>
|
|
<span class="text-muted">{{ dept.compliance_rate }}%</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">Compliance Alerts</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if compliance_alerts %}
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Employee</th>
|
|
<th>Requirement</th>
|
|
<th>Due Date</th>
|
|
<th>Priority</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for alert in compliance_alerts %}
|
|
<tr>
|
|
<td>{{ alert.employee.get_full_name }}</td>
|
|
<td>{{ alert.requirement }}</td>
|
|
<td>
|
|
<span class="text-{{ alert.urgency_color }}">
|
|
{{ alert.due_date|date:"M d, Y" }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{{ alert.priority_color }}">
|
|
{{ alert.get_priority_display }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="resolveAlert('{{ alert.id }}')">
|
|
<i class="fa fa-check"></i> Resolve
|
|
</button>
|
|
<button class="btn btn-outline-warning" onclick="snoozeAlert('{{ alert.id }}')">
|
|
<i class="fa fa-clock"></i> Snooze
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4">
|
|
<i class="fa fa-shield-alt fa-3x text-success mb-3"></i>
|
|
<h5 class="text-success">All Clear!</h5>
|
|
<p class="text-muted">No compliance alerts at this time.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Training Program Modal -->
|
|
<div class="modal fade" id="programModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="programModalTitle">Create Training Program</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="programForm">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="program_id" id="programId">
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="mb-3">
|
|
<label class="form-label">Program Title <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" name="title" id="programTitle" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Category <span class="text-danger">*</span></label>
|
|
<select class="form-select" name="category" id="programCategorySelect" required>
|
|
<option value="">Select category...</option>
|
|
<option value="mandatory">Mandatory</option>
|
|
<option value="clinical">Clinical</option>
|
|
<option value="safety">Safety</option>
|
|
<option value="compliance">Compliance</option>
|
|
<option value="professional">Professional Development</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" name="description" id="programDescription" rows="3"></textarea>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Duration (hours)</label>
|
|
<input type="number" class="form-control" name="duration_hours" id="programDuration" min="0.5" step="0.5">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Max Attendees</label>
|
|
<input type="number" class="form-control" name="max_attendees" id="programMaxAttendees" min="1">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Validity Period (months)</label>
|
|
<input type="number" class="form-control" name="validity_months" id="programValidity" min="1">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="is_mandatory" id="programMandatory">
|
|
<label class="form-check-label" for="programMandatory">
|
|
Mandatory training
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="requires_certification" id="programCertification">
|
|
<label class="form-check-label" for="programCertification">
|
|
Requires certification
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fa fa-save me-2"></i>Save Program
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
setupEventHandlers();
|
|
setupFilters();
|
|
});
|
|
|
|
function setupEventHandlers() {
|
|
// Program form
|
|
$('#programForm').on('submit', function(e) {
|
|
e.preventDefault();
|
|
saveProgram();
|
|
});
|
|
|
|
// Search and filter handlers
|
|
$('#programSearch, #sessionSearch, #certSearch').on('input', function() {
|
|
filterResults();
|
|
});
|
|
|
|
$('#programCategory, #programStatus, #sessionStatus, #certStatus, #certType').on('change', function() {
|
|
filterResults();
|
|
});
|
|
}
|
|
|
|
function setupFilters() {
|
|
// Initialize filter functionality
|
|
$('.training-program-card').each(function() {
|
|
$(this).data('original-display', $(this).css('display'));
|
|
});
|
|
}
|
|
|
|
function createTraining() {
|
|
// Determine which tab is active and create appropriate item
|
|
var activeTab = $('.nav-tabs .nav-link.active').attr('id');
|
|
|
|
switch(activeTab) {
|
|
case 'programs-tab':
|
|
createProgram();
|
|
break;
|
|
case 'sessions-tab':
|
|
scheduleNewSession();
|
|
break;
|
|
case 'certifications-tab':
|
|
addCertification();
|
|
break;
|
|
default:
|
|
createProgram();
|
|
}
|
|
}
|
|
|
|
function createProgram() {
|
|
$('#programModalTitle').text('Create Training Program');
|
|
$('#programId').val('');
|
|
$('#programForm')[0].reset();
|
|
$('#programModal').modal('show');
|
|
}
|
|
|
|
function editProgram(programId) {
|
|
$('#programModalTitle').text('Edit Training Program');
|
|
$('#programId').val(programId);
|
|
|
|
// Load program data
|
|
$.get('{% url "hr:get_program_details" %}', {program_id: programId}, function(data) {
|
|
if (data.success) {
|
|
populateProgramForm(data.program);
|
|
$('#programModal').modal('show');
|
|
} else {
|
|
toastr.error('Failed to load program details');
|
|
}
|
|
});
|
|
}
|
|
|
|
function populateProgramForm(program) {
|
|
$('#programTitle').val(program.title);
|
|
$('#programCategorySelect').val(program.category);
|
|
$('#programDescription').val(program.description);
|
|
$('#programDuration').val(program.duration_hours);
|
|
$('#programMaxAttendees').val(program.max_attendees);
|
|
$('#programValidity').val(program.validity_months);
|
|
$('#programMandatory').prop('checked', program.is_mandatory);
|
|
$('#programCertification').prop('checked', program.requires_certification);
|
|
}
|
|
|
|
function saveProgram() {
|
|
var formData = $('#programForm').serialize();
|
|
var url = $('#programId').val() ? '{% url "hr:update_program" %}' : '{% url "hr:create_program" %}';
|
|
|
|
$.post(url, formData, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Training program saved successfully');
|
|
$('#programModal').modal('hide');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to save program: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function viewProgram(programId) {
|
|
window.location.href = '{% url "hr:program_detail" 0 %}'.replace('0', programId);
|
|
}
|
|
|
|
function scheduleSession(programId) {
|
|
window.location.href = '{% url "hr:schedule_session" 0 %}'.replace('0', programId);
|
|
}
|
|
|
|
function scheduleNewSession() {
|
|
window.location.href = '{% url "hr:schedule_session" %}';
|
|
}
|
|
|
|
function viewSession(sessionId) {
|
|
window.location.href = '{% url "hr:session_detail" 0 %}'.replace('0', sessionId);
|
|
}
|
|
|
|
function editSession(sessionId) {
|
|
window.location.href = '{% url "hr:session_edit" 0 %}'.replace('0', sessionId);
|
|
}
|
|
|
|
function manageAttendees(sessionId) {
|
|
window.location.href = '{% url "hr:session_attendees" 0 %}'.replace('0', sessionId);
|
|
}
|
|
|
|
function startSession(sessionId) {
|
|
$.post('{% url "hr:start_session" %}', {
|
|
session_id: sessionId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Session started');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to start session: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function completeSession(sessionId) {
|
|
$.post('{% url "hr:complete_session" %}', {
|
|
session_id: sessionId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Session completed');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to complete session: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function duplicateSession(sessionId) {
|
|
$.post('{% url "hr:duplicate_session" %}', {
|
|
session_id: sessionId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Session duplicated');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to duplicate session: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function sendReminders(sessionId) {
|
|
$.post('{% url "hr:send_session_reminders" %}', {
|
|
session_id: sessionId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Reminders sent');
|
|
} else {
|
|
toastr.error('Failed to send reminders: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function cancelSession(sessionId) {
|
|
if (confirm('Are you sure you want to cancel this session?')) {
|
|
$.post('{% url "hr:cancel_session" %}', {
|
|
session_id: sessionId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Session cancelled');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to cancel session: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function addCertification() {
|
|
window.location.href = '{% url "hr:add_certification" %}';
|
|
}
|
|
|
|
function viewCertification(certId) {
|
|
window.location.href = '{% url "hr:certification_detail" 0 %}'.replace('0', certId);
|
|
}
|
|
|
|
function editCertification(certId) {
|
|
window.location.href = '{% url "hr:certification_edit" 0 %}'.replace('0', certId);
|
|
}
|
|
|
|
function renewCertification(certId) {
|
|
$.post('{% url "hr:renew_certification" %}', {
|
|
certification_id: certId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Certification renewal initiated');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to renew certification: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function downloadCertificate(certId) {
|
|
window.open('{% url "hr:download_certificate" 0 %}'.replace('0', certId), '_blank');
|
|
}
|
|
|
|
function resolveAlert(alertId) {
|
|
$.post('{% url "hr:resolve_compliance_alert" %}', {
|
|
alert_id: alertId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Alert resolved');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to resolve alert: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function snoozeAlert(alertId) {
|
|
var days = prompt('Snooze for how many days?', '7');
|
|
if (days && !isNaN(days)) {
|
|
$.post('{% url "hr:snooze_compliance_alert" %}', {
|
|
alert_id: alertId,
|
|
days: days,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Alert snoozed for ' + days + ' days');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to snooze alert: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function filterResults() {
|
|
var activeTab = $('.nav-tabs .nav-link.active').attr('id');
|
|
|
|
switch(activeTab) {
|
|
case 'programs-tab':
|
|
filterPrograms();
|
|
break;
|
|
case 'sessions-tab':
|
|
filterSessions();
|
|
break;
|
|
case 'certifications-tab':
|
|
filterCertifications();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function filterPrograms() {
|
|
var searchTerm = $('#programSearch').val().toLowerCase();
|
|
var category = $('#programCategory').val();
|
|
var status = $('#programStatus').val();
|
|
|
|
$('.training-program-card').each(function() {
|
|
var card = $(this);
|
|
var title = card.find('.card-title').text().toLowerCase();
|
|
var description = card.find('.card-text').text().toLowerCase();
|
|
var cardCategory = card.data('category');
|
|
var cardStatus = card.data('status');
|
|
|
|
var matchesSearch = !searchTerm || title.includes(searchTerm) || description.includes(searchTerm);
|
|
var matchesCategory = !category || cardCategory === category;
|
|
var matchesStatus = !status || cardStatus === status;
|
|
|
|
if (matchesSearch && matchesCategory && matchesStatus) {
|
|
card.show();
|
|
} else {
|
|
card.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function filterSessions() {
|
|
// Implement session filtering
|
|
var searchTerm = $('#sessionSearch').val().toLowerCase();
|
|
var status = $('#sessionStatus').val();
|
|
var date = $('#sessionDate').val();
|
|
var instructor = $('#sessionInstructor').val();
|
|
|
|
// Filter table rows based on criteria
|
|
$('tbody tr').each(function() {
|
|
var row = $(this);
|
|
var text = row.text().toLowerCase();
|
|
var rowStatus = row.find('.badge').text().toLowerCase();
|
|
|
|
var matchesSearch = !searchTerm || text.includes(searchTerm);
|
|
var matchesStatus = !status || rowStatus.includes(status.toLowerCase());
|
|
|
|
if (matchesSearch && matchesStatus) {
|
|
row.show();
|
|
} else {
|
|
row.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function filterCertifications() {
|
|
// Implement certification filtering
|
|
var searchTerm = $('#certSearch').val().toLowerCase();
|
|
var status = $('#certStatus').val();
|
|
var type = $('#certType').val();
|
|
var department = $('#certDepartment').val();
|
|
|
|
// Filter table rows based on criteria
|
|
$('tbody tr').each(function() {
|
|
var row = $(this);
|
|
var text = row.text().toLowerCase();
|
|
|
|
var matchesSearch = !searchTerm || text.includes(searchTerm);
|
|
|
|
if (matchesSearch) {
|
|
row.show();
|
|
} else {
|
|
row.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function viewCalendar() {
|
|
window.location.href = '{% url "hr:training_calendar" %}';
|
|
}
|
|
|
|
function generateReport() {
|
|
window.location.href = '{% url "hr:training_reports" %}';
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.training-program-card {
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.training-program-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.avatar-sm {
|
|
width: 24px;
|
|
height: 24px;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.avatar-initial {
|
|
background-color: #6c757d;
|
|
color: white;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.progress {
|
|
background-color: #e9ecef;
|
|
}
|
|
|
|
.progress-bar {
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.nav-tabs .nav-link {
|
|
border: none;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.nav-tabs .nav-link.active {
|
|
background-color: transparent;
|
|
border-bottom: 2px solid #0d6efd;
|
|
color: #0d6efd;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75em;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.table th {
|
|
border-top: none;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.table-hover tbody tr:hover {
|
|
background-color: rgba(0, 0, 0, 0.025);
|
|
}
|
|
|
|
.btn-group-sm .btn {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
border: 1px solid #dee2e6;
|
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
|
}
|
|
|
|
.dropdown-item:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.card-header-tabs {
|
|
margin-bottom: -1px;
|
|
}
|
|
|
|
.card-header-tabs .nav-link {
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
}
|
|
|
|
.card-header-tabs .nav-link.active {
|
|
border-bottom-color: #0d6efd;
|
|
background-color: transparent;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.btn-group {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.btn-group .btn {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.training-program-card {
|
|
margin-bottom: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|