1089 lines
46 KiB
HTML
1089 lines
46 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Schedule 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">Schedule Management</li>
|
|
</ul>
|
|
|
|
<div class="row align-items-center mb-3">
|
|
<div class="col">
|
|
<h1 class="page-header">Schedule Management</h1>
|
|
<p class="text-muted">Employee scheduling and shift management system</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="btn-group">
|
|
<button class="btn btn-primary" onclick="createSchedule()">
|
|
<i class="fa fa-plus me-2"></i>New Schedule
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="importSchedule()">
|
|
<i class="fa fa-upload me-2"></i>Import
|
|
</button>
|
|
<button class="btn btn-outline-info" onclick="exportSchedule()">
|
|
<i class="fa fa-download me-2"></i>Export
|
|
</button>
|
|
<button class="btn btn-outline-success" onclick="publishSchedule()">
|
|
<i class="fa fa-share me-2"></i>Publish
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Schedule 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_employees }}</h4>
|
|
<p class="mb-0">Total Employees</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-users 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">{{ scheduled_today }}</h4>
|
|
<p class="mb-0">Scheduled Today</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-calendar-check 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">{{ open_shifts }}</h4>
|
|
<p class="mb-0">Open Shifts</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-exclamation-triangle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-info text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h4 class="mb-0">{{ coverage_percentage }}%</h4>
|
|
<p class="mb-0">Coverage Rate</p>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="fa fa-chart-pie fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Schedule Controls -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">Schedule Controls</h4>
|
|
<div class="card-tools">
|
|
<div class="btn-group">
|
|
<button class="btn btn-outline-primary btn-sm" onclick="autoSchedule()">
|
|
<i class="fa fa-magic me-1"></i>Auto Schedule
|
|
</button>
|
|
<button class="btn btn-outline-warning btn-sm" onclick="checkConflicts()">
|
|
<i class="fa fa-exclamation-triangle me-1"></i>Check Conflicts
|
|
</button>
|
|
<button class="btn btn-outline-info btn-sm" onclick="optimizeSchedule()">
|
|
<i class="fa fa-cogs me-1"></i>Optimize
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-2">
|
|
<label class="form-label">View Type</label>
|
|
<select class="form-select" id="viewType" onchange="changeView()">
|
|
<option value="week" {% if view_type == 'week' %}selected{% endif %}>Week View</option>
|
|
<option value="month" {% if view_type == 'month' %}selected{% endif %}>Month View</option>
|
|
<option value="day" {% if view_type == 'day' %}selected{% endif %}>Day View</option>
|
|
<option value="employee" {% if view_type == 'employee' %}selected{% endif %}>Employee View</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Department</label>
|
|
<select class="form-select" id="departmentFilter" onchange="filterSchedule()">
|
|
<option value="">All Departments</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}" {% if selected_department == dept.id|stringformat:"s" %}selected{% endif %}>{{ dept.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Shift Type</label>
|
|
<select class="form-select" id="shiftFilter" onchange="filterSchedule()">
|
|
<option value="">All Shifts</option>
|
|
<option value="day" {% if selected_shift == 'day' %}selected{% endif %}>Day Shift</option>
|
|
<option value="evening" {% if selected_shift == 'evening' %}selected{% endif %}>Evening Shift</option>
|
|
<option value="night" {% if selected_shift == 'night' %}selected{% endif %}>Night Shift</option>
|
|
<option value="weekend" {% if selected_shift == 'weekend' %}selected{% endif %}>Weekend</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Status</label>
|
|
<select class="form-select" id="statusFilter" onchange="filterSchedule()">
|
|
<option value="">All Status</option>
|
|
<option value="draft" {% if selected_status == 'draft' %}selected{% endif %}>Draft</option>
|
|
<option value="published" {% if selected_status == 'published' %}selected{% endif %}>Published</option>
|
|
<option value="confirmed" {% if selected_status == 'confirmed' %}selected{% endif %}>Confirmed</option>
|
|
<option value="completed" {% if selected_status == 'completed' %}selected{% endif %}>Completed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Week Starting</label>
|
|
<input type="date" class="form-control" id="weekStart" value="{{ current_week_start }}" onchange="changeWeek()">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label"> </label>
|
|
<div class="btn-group w-100">
|
|
<button class="btn btn-outline-secondary" onclick="previousPeriod()">
|
|
<i class="fa fa-chevron-left"></i>
|
|
</button>
|
|
<button class="btn btn-outline-primary" onclick="goToToday()">Today</button>
|
|
<button class="btn btn-outline-secondary" onclick="nextPeriod()">
|
|
<i class="fa fa-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Schedule Calendar -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">
|
|
Schedule Calendar
|
|
<span class="badge bg-secondary ms-2">{{ current_period_display }}</span>
|
|
</h4>
|
|
<div class="card-tools">
|
|
<div class="btn-group">
|
|
<button class="btn btn-outline-primary btn-sm" onclick="addShift()">
|
|
<i class="fa fa-plus me-1"></i>Add Shift
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="copyWeek()">
|
|
<i class="fa fa-copy me-1"></i>Copy Week
|
|
</button>
|
|
<button class="btn btn-outline-info btn-sm" onclick="printSchedule()">
|
|
<i class="fa fa-print me-1"></i>Print
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Week View -->
|
|
<div id="weekView" class="schedule-view">
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered schedule-table">
|
|
<thead>
|
|
<tr>
|
|
<th width="150">Employee</th>
|
|
{% for day in week_days %}
|
|
<th class="text-center">
|
|
<div>{{ day.name }}</div>
|
|
<small class="text-muted">{{ day.date|date:"M d" }}</small>
|
|
</th>
|
|
{% endfor %}
|
|
<th width="80">Total Hours</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for employee in employees %}
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar avatar-sm me-2">
|
|
{% if employee.photo %}
|
|
<img src="{{ employee.photo.url }}" alt="{{ employee.get_full_name }}" class="rounded-circle">
|
|
{% else %}
|
|
<div class="avatar-initial rounded-circle bg-primary">
|
|
{{ employee.first_name|first }}{{ employee.last_name|first }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<h6 class="mb-0">{{ employee.get_full_name }}</h6>
|
|
<small class="text-muted">{{ employee.job_title }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
{% for day in week_days %}
|
|
<td class="schedule-cell" data-employee="{{ employee.id }}" data-date="{{ day.date|date:'Y-m-d' }}">
|
|
{% for shift in employee.shifts_for_day %}
|
|
{% if shift.date == day.date %}
|
|
<div class="shift-block shift-{{ shift.type }}"
|
|
onclick="editShift('{{ shift.id }}')"
|
|
title="{{ shift.start_time }} - {{ shift.end_time }}">
|
|
<div class="shift-time">{{ shift.start_time|time:"H:i" }} - {{ shift.end_time|time:"H:i" }}</div>
|
|
<div class="shift-location">{{ shift.location|default:"" }}</div>
|
|
{% if shift.status == 'open' %}
|
|
<span class="badge badge-warning">Open</span>
|
|
{% elif shift.status == 'conflict' %}
|
|
<span class="badge badge-danger">Conflict</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
<button class="btn btn-outline-primary btn-sm add-shift-btn"
|
|
onclick="addShiftForEmployee('{{ employee.id }}', '{{ day.date|date:'Y-m-d' }}')"
|
|
title="Add shift">
|
|
<i class="fa fa-plus"></i>
|
|
</button>
|
|
</td>
|
|
{% endfor %}
|
|
<td class="text-center">
|
|
<strong>{{ employee.total_hours|default:"0" }}h</strong>
|
|
{% if employee.overtime_hours %}
|
|
<br><small class="text-warning">+{{ employee.overtime_hours }}h OT</small>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="table-secondary">
|
|
<th>Daily Totals</th>
|
|
{% for day in week_days %}
|
|
<th class="text-center">
|
|
<div>{{ day.total_employees }} staff</div>
|
|
<small class="text-muted">{{ day.total_hours }}h</small>
|
|
</th>
|
|
{% endfor %}
|
|
<th class="text-center">{{ week_total_hours }}h</th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shift Legend -->
|
|
<div class="mt-3">
|
|
<div class="d-flex flex-wrap gap-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="shift-legend shift-day me-2"></div>
|
|
<span>Day Shift (7AM-7PM)</span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<div class="shift-legend shift-evening me-2"></div>
|
|
<span>Evening Shift (3PM-11PM)</span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<div class="shift-legend shift-night me-2"></div>
|
|
<span>Night Shift (11PM-7AM)</span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<div class="shift-legend shift-weekend me-2"></div>
|
|
<span>Weekend Shift</span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<div class="shift-legend shift-oncall me-2"></div>
|
|
<span>On-Call</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Schedule Conflicts -->
|
|
{% if conflicts %}
|
|
<div class="row mt-4">
|
|
<div class="col-12">
|
|
<div class="card border-warning">
|
|
<div class="card-header bg-warning text-dark">
|
|
<h4 class="card-title mb-0">
|
|
<i class="fa fa-exclamation-triangle me-2"></i>Schedule Conflicts
|
|
</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Employee</th>
|
|
<th>Date</th>
|
|
<th>Conflict Type</th>
|
|
<th>Details</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for conflict in conflicts %}
|
|
<tr>
|
|
<td>{{ conflict.employee.get_full_name }}</td>
|
|
<td>{{ conflict.date|date:"M d, Y" }}</td>
|
|
<td>
|
|
<span class="badge bg-warning">{{ conflict.get_type_display }}</span>
|
|
</td>
|
|
<td>{{ conflict.description }}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" onclick="resolveConflict('{{ conflict.id }}')">
|
|
<i class="fa fa-check"></i> Resolve
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="viewConflictDetails('{{ conflict.id }}')">
|
|
<i class="fa fa-eye"></i> Details
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add/Edit Shift Modal -->
|
|
<div class="modal fade" id="shiftModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="shiftModalTitle">Add Shift</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="shiftForm">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="shift_id" id="shiftId">
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Employee <span class="text-danger">*</span></label>
|
|
<select class="form-select" name="employee" id="shiftEmployee" required>
|
|
<option value="">Select employee...</option>
|
|
{% for employee in all_employees %}
|
|
<option value="{{ employee.id }}">{{ employee.get_full_name }} ({{ employee.job_title }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Date <span class="text-danger">*</span></label>
|
|
<input type="date" class="form-control" name="date" id="shiftDate" required>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Start Time <span class="text-danger">*</span></label>
|
|
<input type="time" class="form-control" name="start_time" id="shiftStartTime" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">End Time <span class="text-danger">*</span></label>
|
|
<input type="time" class="form-control" name="end_time" id="shiftEndTime" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Shift Type</label>
|
|
<select class="form-select" name="shift_type" id="shiftType">
|
|
<option value="day">Day Shift</option>
|
|
<option value="evening">Evening Shift</option>
|
|
<option value="night">Night Shift</option>
|
|
<option value="weekend">Weekend Shift</option>
|
|
<option value="oncall">On-Call</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Location</label>
|
|
<select class="form-select" name="location" id="shiftLocation">
|
|
<option value="">Select location...</option>
|
|
<option value="emergency">Emergency Department</option>
|
|
<option value="icu">ICU</option>
|
|
<option value="surgery">Surgery</option>
|
|
<option value="medical">Medical Ward</option>
|
|
<option value="pediatrics">Pediatrics</option>
|
|
<option value="maternity">Maternity</option>
|
|
<option value="outpatient">Outpatient</option>
|
|
<option value="pharmacy">Pharmacy</option>
|
|
<option value="lab">Laboratory</option>
|
|
<option value="radiology">Radiology</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Role</label>
|
|
<input type="text" class="form-control" name="role" id="shiftRole" placeholder="e.g., Charge Nurse, Resident, etc.">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Break Duration (minutes)</label>
|
|
<input type="number" class="form-control" name="break_duration" id="shiftBreakDuration" value="30" min="0" max="120">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Status</label>
|
|
<select class="form-select" name="status" id="shiftStatus">
|
|
<option value="draft">Draft</option>
|
|
<option value="published">Published</option>
|
|
<option value="confirmed">Confirmed</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes</label>
|
|
<textarea class="form-control" name="notes" id="shiftNotes" rows="3" placeholder="Additional notes or special instructions..."></textarea>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="is_overtime" id="shiftIsOvertime">
|
|
<label class="form-check-label" for="shiftIsOvertime">
|
|
Overtime shift
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="requires_certification" id="shiftRequiresCert">
|
|
<label class="form-check-label" for="shiftRequiresCert">
|
|
Requires special 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" id="shiftSubmitBtn">
|
|
<i class="fa fa-save me-2"></i>Save Shift
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Copy Week Modal -->
|
|
<div class="modal fade" id="copyWeekModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Copy Week Schedule</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="copyWeekForm">
|
|
{% csrf_token %}
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Source Week</label>
|
|
<input type="date" class="form-control" name="source_week" value="{{ current_week_start }}">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Target Week(s)</label>
|
|
<input type="date" class="form-control" name="target_week" required>
|
|
<div class="form-text">Select the Monday of the target week</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Copy Options</label>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="copy_employees" id="copyEmployees" checked>
|
|
<label class="form-check-label" for="copyEmployees">
|
|
Copy employee assignments
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="copy_times" id="copyTimes" checked>
|
|
<label class="form-check-label" for="copyTimes">
|
|
Copy shift times
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="copy_locations" id="copyLocations" checked>
|
|
<label class="form-check-label" for="copyLocations">
|
|
Copy locations and roles
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Department Filter</label>
|
|
<select class="form-select" name="department_filter">
|
|
<option value="">All departments</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}">{{ dept.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</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-copy me-2"></i>Copy Schedule
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
setupEventHandlers();
|
|
setupDragAndDrop();
|
|
loadScheduleData();
|
|
});
|
|
|
|
function setupEventHandlers() {
|
|
// Shift form
|
|
$('#shiftForm').on('submit', function(e) {
|
|
e.preventDefault();
|
|
saveShift();
|
|
});
|
|
|
|
// Copy week form
|
|
$('#copyWeekForm').on('submit', function(e) {
|
|
e.preventDefault();
|
|
copyWeekSchedule();
|
|
});
|
|
|
|
// Auto-calculate shift type based on time
|
|
$('#shiftStartTime, #shiftEndTime').on('change', function() {
|
|
autoDetectShiftType();
|
|
});
|
|
}
|
|
|
|
function setupDragAndDrop() {
|
|
// Enable drag and drop for shift blocks
|
|
$('.shift-block').draggable({
|
|
helper: 'clone',
|
|
revert: 'invalid',
|
|
start: function(event, ui) {
|
|
$(this).addClass('dragging');
|
|
},
|
|
stop: function(event, ui) {
|
|
$(this).removeClass('dragging');
|
|
}
|
|
});
|
|
|
|
$('.schedule-cell').droppable({
|
|
accept: '.shift-block',
|
|
hoverClass: 'drop-hover',
|
|
drop: function(event, ui) {
|
|
var shiftId = ui.draggable.data('shift-id');
|
|
var employeeId = $(this).data('employee');
|
|
var date = $(this).data('date');
|
|
moveShift(shiftId, employeeId, date);
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadScheduleData() {
|
|
// Load additional schedule data via AJAX
|
|
var params = {
|
|
view_type: $('#viewType').val(),
|
|
department: $('#departmentFilter').val(),
|
|
shift_type: $('#shiftFilter').val(),
|
|
status: $('#statusFilter').val(),
|
|
week_start: $('#weekStart').val()
|
|
};
|
|
|
|
$.get('{% url "hr:schedule_data" %}', params, function(data) {
|
|
if (data.success) {
|
|
updateScheduleDisplay(data);
|
|
}
|
|
});
|
|
}
|
|
|
|
function createSchedule() {
|
|
$('#shiftModalTitle').text('Add New Shift');
|
|
$('#shiftId').val('');
|
|
$('#shiftForm')[0].reset();
|
|
$('#shiftModal').modal('show');
|
|
}
|
|
|
|
function addShift() {
|
|
createSchedule();
|
|
}
|
|
|
|
function addShiftForEmployee(employeeId, date) {
|
|
$('#shiftModalTitle').text('Add Shift');
|
|
$('#shiftId').val('');
|
|
$('#shiftForm')[0].reset();
|
|
$('#shiftEmployee').val(employeeId);
|
|
$('#shiftDate').val(date);
|
|
$('#shiftModal').modal('show');
|
|
}
|
|
|
|
function editShift(shiftId) {
|
|
$('#shiftModalTitle').text('Edit Shift');
|
|
$('#shiftId').val(shiftId);
|
|
|
|
// Load shift data
|
|
$.get('{% url "hr:get_shift_details" %}', {shift_id: shiftId}, function(data) {
|
|
if (data.success) {
|
|
populateShiftForm(data.shift);
|
|
$('#shiftModal').modal('show');
|
|
} else {
|
|
toastr.error('Failed to load shift details');
|
|
}
|
|
});
|
|
}
|
|
|
|
function populateShiftForm(shift) {
|
|
$('#shiftEmployee').val(shift.employee_id);
|
|
$('#shiftDate').val(shift.date);
|
|
$('#shiftStartTime').val(shift.start_time);
|
|
$('#shiftEndTime').val(shift.end_time);
|
|
$('#shiftType').val(shift.shift_type);
|
|
$('#shiftLocation').val(shift.location);
|
|
$('#shiftRole').val(shift.role);
|
|
$('#shiftBreakDuration').val(shift.break_duration);
|
|
$('#shiftStatus').val(shift.status);
|
|
$('#shiftNotes').val(shift.notes);
|
|
$('#shiftIsOvertime').prop('checked', shift.is_overtime);
|
|
$('#shiftRequiresCert').prop('checked', shift.requires_certification);
|
|
}
|
|
|
|
function saveShift() {
|
|
var formData = $('#shiftForm').serialize();
|
|
var url = $('#shiftId').val() ? '{% url "hr:update_shift" %}' : '{% url "hr:create_shift" %}';
|
|
|
|
$.post(url, formData, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Shift saved successfully');
|
|
$('#shiftModal').modal('hide');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to save shift: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function moveShift(shiftId, employeeId, date) {
|
|
$.post('{% url "hr:move_shift" %}', {
|
|
shift_id: shiftId,
|
|
employee_id: employeeId,
|
|
date: date,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Shift moved successfully');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to move shift: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function autoDetectShiftType() {
|
|
var startTime = $('#shiftStartTime').val();
|
|
var endTime = $('#shiftEndTime').val();
|
|
|
|
if (startTime && endTime) {
|
|
var start = new Date('2000-01-01 ' + startTime);
|
|
var end = new Date('2000-01-01 ' + endTime);
|
|
|
|
if (start.getHours() >= 7 && start.getHours() < 15) {
|
|
$('#shiftType').val('day');
|
|
} else if (start.getHours() >= 15 && start.getHours() < 23) {
|
|
$('#shiftType').val('evening');
|
|
} else {
|
|
$('#shiftType').val('night');
|
|
}
|
|
}
|
|
}
|
|
|
|
function changeView() {
|
|
var viewType = $('#viewType').val();
|
|
var params = new URLSearchParams(window.location.search);
|
|
params.set('view_type', viewType);
|
|
window.location.href = '{% url "hr:schedule_management" %}?' + params.toString();
|
|
}
|
|
|
|
function filterSchedule() {
|
|
var params = new URLSearchParams();
|
|
params.set('department', $('#departmentFilter').val());
|
|
params.set('shift_type', $('#shiftFilter').val());
|
|
params.set('status', $('#statusFilter').val());
|
|
params.set('week_start', $('#weekStart').val());
|
|
window.location.href = '{% url "hr:schedule_management" %}?' + params.toString();
|
|
}
|
|
|
|
function changeWeek() {
|
|
var weekStart = $('#weekStart').val();
|
|
var params = new URLSearchParams(window.location.search);
|
|
params.set('week_start', weekStart);
|
|
window.location.href = '{% url "hr:schedule_management" %}?' + params.toString();
|
|
}
|
|
|
|
function previousPeriod() {
|
|
var currentDate = new Date($('#weekStart').val());
|
|
currentDate.setDate(currentDate.getDate() - 7);
|
|
$('#weekStart').val(currentDate.toISOString().split('T')[0]);
|
|
changeWeek();
|
|
}
|
|
|
|
function nextPeriod() {
|
|
var currentDate = new Date($('#weekStart').val());
|
|
currentDate.setDate(currentDate.getDate() + 7);
|
|
$('#weekStart').val(currentDate.toISOString().split('T')[0]);
|
|
changeWeek();
|
|
}
|
|
|
|
function goToToday() {
|
|
var today = new Date();
|
|
var monday = new Date(today.setDate(today.getDate() - today.getDay() + 1));
|
|
$('#weekStart').val(monday.toISOString().split('T')[0]);
|
|
changeWeek();
|
|
}
|
|
|
|
function autoSchedule() {
|
|
$.post('{% url "hr:auto_schedule" %}', {
|
|
week_start: $('#weekStart').val(),
|
|
department: $('#departmentFilter').val(),
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Auto-scheduling completed');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Auto-scheduling failed: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function checkConflicts() {
|
|
$.post('{% url "hr:check_schedule_conflicts" %}', {
|
|
week_start: $('#weekStart').val(),
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
if (data.conflicts.length > 0) {
|
|
toastr.warning(data.conflicts.length + ' conflicts found');
|
|
displayConflicts(data.conflicts);
|
|
} else {
|
|
toastr.success('No conflicts found');
|
|
}
|
|
} else {
|
|
toastr.error('Failed to check conflicts');
|
|
}
|
|
});
|
|
}
|
|
|
|
function optimizeSchedule() {
|
|
$.post('{% url "hr:optimize_schedule" %}', {
|
|
week_start: $('#weekStart').val(),
|
|
department: $('#departmentFilter').val(),
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Schedule optimized');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Optimization failed: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function publishSchedule() {
|
|
if (confirm('Are you sure you want to publish this schedule? Employees will be notified.')) {
|
|
$.post('{% url "hr:publish_schedule" %}', {
|
|
week_start: $('#weekStart').val(),
|
|
department: $('#departmentFilter').val(),
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Schedule published successfully');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to publish schedule: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function copyWeek() {
|
|
$('#copyWeekModal').modal('show');
|
|
}
|
|
|
|
function copyWeekSchedule() {
|
|
var formData = $('#copyWeekForm').serialize();
|
|
|
|
$.post('{% url "hr:copy_week_schedule" %}', formData, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Week schedule copied successfully');
|
|
$('#copyWeekModal').modal('hide');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to copy schedule: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function printSchedule() {
|
|
var params = new URLSearchParams(window.location.search);
|
|
params.set('print', 'true');
|
|
window.open('{% url "hr:schedule_management" %}?' + params.toString(), '_blank');
|
|
}
|
|
|
|
function importSchedule() {
|
|
// Implement schedule import functionality
|
|
toastr.info('Schedule import feature coming soon');
|
|
}
|
|
|
|
function exportSchedule() {
|
|
var params = new URLSearchParams(window.location.search);
|
|
params.set('export', 'true');
|
|
window.location.href = '{% url "hr:schedule_management" %}?' + params.toString();
|
|
}
|
|
|
|
function resolveConflict(conflictId) {
|
|
$.post('{% url "hr:resolve_schedule_conflict" %}', {
|
|
conflict_id: conflictId,
|
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
|
}, function(data) {
|
|
if (data.success) {
|
|
toastr.success('Conflict resolved');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to resolve conflict: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function viewConflictDetails(conflictId) {
|
|
// Implement conflict details view
|
|
toastr.info('Conflict details feature coming soon');
|
|
}
|
|
|
|
function updateScheduleDisplay(data) {
|
|
// Update the schedule display with new data
|
|
// This would be implemented based on the specific data structure
|
|
}
|
|
|
|
function displayConflicts(conflicts) {
|
|
// Display conflicts in a modal or alert
|
|
var conflictList = conflicts.map(function(conflict) {
|
|
return conflict.employee + ': ' + conflict.description;
|
|
}).join('\n');
|
|
|
|
alert('Schedule Conflicts:\n\n' + conflictList);
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.schedule-table {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.schedule-cell {
|
|
min-height: 80px;
|
|
vertical-align: top;
|
|
position: relative;
|
|
padding: 4px;
|
|
}
|
|
|
|
.shift-block {
|
|
background: #e3f2fd;
|
|
border: 1px solid #2196f3;
|
|
border-radius: 4px;
|
|
padding: 4px 6px;
|
|
margin-bottom: 2px;
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
position: relative;
|
|
}
|
|
|
|
.shift-block:hover {
|
|
background: #bbdefb;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.shift-day { background: #e8f5e8; border-color: #4caf50; }
|
|
.shift-evening { background: #fff3e0; border-color: #ff9800; }
|
|
.shift-night { background: #e1f5fe; border-color: #03a9f4; }
|
|
.shift-weekend { background: #f3e5f5; border-color: #9c27b0; }
|
|
.shift-oncall { background: #fce4ec; border-color: #e91e63; }
|
|
|
|
.shift-time {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.shift-location {
|
|
font-size: 0.7rem;
|
|
color: #666;
|
|
}
|
|
|
|
.shift-legend {
|
|
width: 20px;
|
|
height: 12px;
|
|
border-radius: 2px;
|
|
display: inline-block;
|
|
}
|
|
|
|
.add-shift-btn {
|
|
position: absolute;
|
|
bottom: 2px;
|
|
right: 2px;
|
|
width: 24px;
|
|
height: 24px;
|
|
padding: 0;
|
|
font-size: 0.7rem;
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.schedule-cell:hover .add-shift-btn {
|
|
opacity: 1;
|
|
}
|
|
|
|
.drop-hover {
|
|
background-color: #f0f8ff;
|
|
border: 2px dashed #2196f3;
|
|
}
|
|
|
|
.dragging {
|
|
opacity: 0.5;
|
|
transform: rotate(5deg);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.card-tools {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.7rem;
|
|
padding: 2px 6px;
|
|
}
|
|
|
|
.badge-warning {
|
|
background-color: #ffc107;
|
|
color: #000;
|
|
}
|
|
|
|
.badge-danger {
|
|
background-color: #dc3545;
|
|
color: #fff;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.schedule-table {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.shift-block {
|
|
font-size: 0.7rem;
|
|
padding: 2px 4px;
|
|
}
|
|
|
|
.schedule-cell {
|
|
min-height: 60px;
|
|
}
|
|
|
|
.btn-group {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.btn-group .btn {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
}
|
|
|
|
.table-responsive {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.schedule-view {
|
|
min-height: 400px;
|
|
}
|
|
|
|
.conflict-highlight {
|
|
background-color: #ffebee !important;
|
|
border: 2px solid #f44336 !important;
|
|
}
|
|
|
|
.overtime-highlight {
|
|
background-color: #fff8e1 !important;
|
|
border: 2px solid #ff9800 !important;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|