560 lines
20 KiB
HTML
560 lines
20 KiB
HTML
{% extends "admin/change_form.html" %}
|
||
{% load static %}
|
||
{% load mathfilters %}
|
||
|
||
{% block extrahead %}
|
||
{{ block.super }}
|
||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||
<style>
|
||
#content {
|
||
width: auto !important;
|
||
padding: 0px !important;
|
||
max-width: auto !important;
|
||
}
|
||
|
||
.content {
|
||
padding: 0px !important;
|
||
margin: 0 !important;
|
||
}
|
||
|
||
.card {
|
||
margin: 0 !important;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.filters-row {
|
||
background: #fff;
|
||
padding: 1rem;
|
||
border-radius: 4px;
|
||
margin-bottom: 1rem;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.table {
|
||
background: white;
|
||
border: 1px solid #ddd;
|
||
width: 100%;
|
||
}
|
||
|
||
.select2-container {
|
||
width: 100% !important;
|
||
}
|
||
|
||
.card-header {
|
||
padding: 1rem;
|
||
margin-bottom: 0;
|
||
background-color: transparent;
|
||
border-bottom: 1px solid #ddd;
|
||
}
|
||
|
||
.card-title {
|
||
margin: 0;
|
||
font-size: 1.25rem;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.card-body {
|
||
flex: 1 1 auto;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.form-control {
|
||
display: block;
|
||
width: 100%;
|
||
padding: .375rem .75rem;
|
||
font-size: 1rem;
|
||
font-weight: 400;
|
||
line-height: 1.5;
|
||
color: #495057;
|
||
background-color: #fff;
|
||
background-clip: padding-box;
|
||
border: 1px solid #ced4da;
|
||
border-radius: .25rem;
|
||
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||
}
|
||
|
||
.select2-container--default .select2-selection--single {
|
||
height: calc(2.25rem + 2px);
|
||
padding: .375rem .75rem;
|
||
border: 1px solid #ced4da;
|
||
}
|
||
|
||
.table th,
|
||
.table td {
|
||
padding: .75rem;
|
||
vertical-align: middle;
|
||
border-top: 1px solid #dee2e6;
|
||
}
|
||
|
||
.table thead th {
|
||
vertical-align: bottom;
|
||
border-bottom: 2px solid #dee2e6;
|
||
background-color: #f4f6f9;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-block;
|
||
font-weight: 400;
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
user-select: none;
|
||
padding: .375rem .75rem;
|
||
font-size: 1rem;
|
||
line-height: 1.5;
|
||
border-radius: .25rem;
|
||
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||
}
|
||
|
||
.btn-primary {
|
||
color: #fff;
|
||
background-color: #007bff;
|
||
border-color: #007bff;
|
||
}
|
||
|
||
.btn-success {
|
||
color: #fff;
|
||
background-color: #28a745;
|
||
border-color: #28a745;
|
||
}
|
||
|
||
.remove-row-btn {
|
||
color: #dc3545;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.alert {
|
||
position: relative;
|
||
padding: .75rem 1.25rem;
|
||
margin-bottom: 1rem;
|
||
border: 1px solid transparent;
|
||
border-radius: .25rem;
|
||
}
|
||
|
||
.alert-success {
|
||
color: #155724;
|
||
background-color: #d4edda;
|
||
border-color: #c3e6cb;
|
||
}
|
||
|
||
.alert-danger {
|
||
color: #721c24;
|
||
background-color: #f8d7da;
|
||
border-color: #f5c6cb;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">Add Routines</h3>
|
||
</div>
|
||
<div class="card-body">
|
||
<form method="post" id="routine-form">
|
||
{% csrf_token %}
|
||
|
||
<div class="filters-row">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="form-group">
|
||
<label for="id_course">Course</label>
|
||
<select name="course" id="id_course" class="form-control" required>
|
||
<option value="">Select Course</option>
|
||
{% for course in adminform.form.fields.course.queryset %}
|
||
{% if course.duration and course.duration_type %}
|
||
{% with total_semesters=course.duration|mul:2 %}
|
||
<option value="{{ course.id }}"
|
||
data-duration="{{ total_semesters }}"
|
||
data-duration-type="Semester">
|
||
{{ course.name }}
|
||
</option>
|
||
{% endwith %}
|
||
{% else %}
|
||
<option value="{{ course.id }}">{{ course.name }}</option>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="form-group">
|
||
<label for="id_period_or_year">Period/Year</label>
|
||
<select name="period_or_year" id="id_period_or_year" class="form-control" required disabled>
|
||
<option value="">Select Course First</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table" id="routine-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Subject</th>
|
||
<th>Teacher</th>
|
||
<th>Start Time</th>
|
||
<th>End Time</th>
|
||
<th style="width: 50px;">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="routine-row">
|
||
<td colspan="5" class="text-center">
|
||
Please select course and period
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="row mt-3">
|
||
<div class="col-12">
|
||
<button type="submit" class="btn btn-primary float-right">
|
||
<i class="fas fa-save"></i> Save All Routines
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const courseSelect = document.getElementById('id_course');
|
||
const periodSelect = document.getElementById('id_period_or_year');
|
||
const routineTable = document.getElementById('routine-table');
|
||
const addRowBtn = document.querySelector('.btn-add');
|
||
const form = document.getElementById('routine-form');
|
||
|
||
// Function to show status message
|
||
function showStatus(message, isError = false) {
|
||
const statusDiv = document.createElement('div');
|
||
statusDiv.className = `alert ${isError ? 'alert-danger' : 'alert-success'}`;
|
||
statusDiv.textContent = message;
|
||
|
||
// Insert at the top of the card body
|
||
const cardBody = document.querySelector('.card-body');
|
||
cardBody.insertBefore(statusDiv, cardBody.firstChild);
|
||
|
||
setTimeout(() => {
|
||
statusDiv.remove();
|
||
}, 5000);
|
||
}
|
||
|
||
// Function to update subject options
|
||
function updateSubjectOptions() {
|
||
const courseId = courseSelect.value;
|
||
const period = periodSelect.value;
|
||
const tbody = routineTable.querySelector('tbody');
|
||
|
||
if (courseId && period) {
|
||
// Show loading state
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="5" class="text-center">
|
||
<div>Loading subjects...</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
|
||
fetch(`/app/get-subjects/?course_id=${courseId}&period_or_year=${period}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.subjects && data.subjects.length > 0) {
|
||
// Clear tbody
|
||
tbody.innerHTML = '';
|
||
|
||
// Create a row for each subject
|
||
data.subjects.forEach(subject => {
|
||
const row = document.createElement('tr');
|
||
row.className = 'routine-row';
|
||
row.innerHTML = `
|
||
<td>
|
||
<input type="hidden" name="subjects[]" value="${subject.id}">
|
||
<span>${subject.name}</span>
|
||
</td>
|
||
<td>
|
||
<select name="teachers[]" class="teacher-select" required>
|
||
<option value="">Select Teacher</option>
|
||
</select>
|
||
</td>
|
||
<td>
|
||
<input type="time" name="start_times[]" required>
|
||
</td>
|
||
<td>
|
||
<input type="time" name="end_times[]" required>
|
||
</td>
|
||
<td>
|
||
<span class="remove-row-btn">×</span>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
// Initialize teacher dropdowns
|
||
updateTeacherOptions();
|
||
|
||
// Initialize Select2 for new teacher dropdowns
|
||
$('.teacher-select').select2({
|
||
width: '100%'
|
||
});
|
||
} else {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="5" class="text-center">
|
||
<div>No subjects available for this period</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching subjects:', error);
|
||
showStatus('Error loading subjects', true);
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="5" class="text-center">
|
||
<div>Error loading subjects</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
} else {
|
||
tbody.innerHTML = `
|
||
<tr class="routine-row">
|
||
<td colspan="5" class="text-center">
|
||
<div>Please select both course and period</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Function to update teacher options
|
||
function updateTeacherOptions() {
|
||
const teacherSelects = document.querySelectorAll('.teacher-select');
|
||
|
||
// Disable teacher selects while loading
|
||
teacherSelects.forEach(select => {
|
||
select.disabled = true;
|
||
select.innerHTML = '<option value="">Loading teachers...</option>';
|
||
});
|
||
|
||
fetch('/app/get-teachers/')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.teachers) {
|
||
teacherSelects.forEach(select => {
|
||
select.innerHTML = '<option value="">Select Teacher</option>';
|
||
data.teachers.forEach(teacher => {
|
||
select.innerHTML += `<option value="${teacher.id}">${teacher.name}</option>`;
|
||
});
|
||
select.disabled = false;
|
||
});
|
||
} else {
|
||
throw new Error('No teachers data returned');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading teachers:', error);
|
||
showStatus('Error loading teachers', true);
|
||
teacherSelects.forEach(select => {
|
||
select.disabled = false;
|
||
select.innerHTML = '<option value="">Error loading teachers</option>';
|
||
});
|
||
});
|
||
}
|
||
|
||
// Function to update period options based on course
|
||
function updatePeriodOptions() {
|
||
const selectedOption = courseSelect.options[courseSelect.selectedIndex];
|
||
console.log('Selected course:', selectedOption.text);
|
||
console.log('Duration:', selectedOption.dataset.duration);
|
||
console.log('Duration type:', selectedOption.dataset['duration-type']);
|
||
|
||
if (courseSelect.value) {
|
||
const durationType = selectedOption.dataset['duration-type'];
|
||
const totalPeriods = parseInt(selectedOption.dataset.duration) || 0;
|
||
const isSemester = durationType === 'Semester';
|
||
|
||
// Calculate total periods based on duration type
|
||
const actualTotalPeriods = isSemester ? totalPeriods * 2 : totalPeriods;
|
||
console.log('Total periods:', actualTotalPeriods);
|
||
|
||
// Clear and update the period select
|
||
periodSelect.innerHTML = '<option value="">Select Period</option>';
|
||
|
||
if (actualTotalPeriods > 0) {
|
||
for (let i = 1; i <= actualTotalPeriods; i++) {
|
||
const option = document.createElement('option');
|
||
option.value = i;
|
||
option.textContent = `${isSemester ? 'Semester' : 'Year'} ${i}`;
|
||
periodSelect.appendChild(option);
|
||
}
|
||
periodSelect.disabled = false;
|
||
} else {
|
||
console.log('No valid duration found for course');
|
||
periodSelect.disabled = true;
|
||
}
|
||
} else {
|
||
periodSelect.innerHTML = '<option value="">Select Course First</option>';
|
||
periodSelect.disabled = true;
|
||
}
|
||
|
||
// Initialize or update Select2
|
||
$(periodSelect).select2({
|
||
width: '100%'
|
||
});
|
||
}
|
||
|
||
// Update subjects when course or period changes
|
||
$(courseSelect).on('change', function() {
|
||
console.log('Course changed:', this.value);
|
||
updatePeriodOptions();
|
||
updateSubjectOptions();
|
||
});
|
||
|
||
$(periodSelect).on('change', function() {
|
||
console.log('Period changed:', this.value);
|
||
updateSubjectOptions();
|
||
});
|
||
|
||
// Event delegation for remove row button
|
||
$(routineTable).on('click', '.remove-row-btn', function() {
|
||
const row = $(this).closest('.routine-row');
|
||
if (confirm('Are you sure you want to remove this routine?')) {
|
||
row.remove();
|
||
if (routineTable.querySelector('tbody').children.length === 0) {
|
||
routineTable.querySelector('tbody').innerHTML = `
|
||
<tr class="routine-row">
|
||
<td colspan="5" class="text-center">
|
||
<div>All routines removed. Please select a course and period to add more.</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
}
|
||
});
|
||
|
||
// Remove the "Add Another Row" button since rows are auto-generated
|
||
if (addRowBtn) {
|
||
addRowBtn.style.display = 'none';
|
||
}
|
||
|
||
// Form submission
|
||
form.addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
|
||
if (!courseSelect.value || !periodSelect.value) {
|
||
showStatus('Please select both Course and Period/Year', true);
|
||
return;
|
||
}
|
||
|
||
const routines = [];
|
||
|
||
// Get all routine rows
|
||
const routineRows = document.querySelectorAll('.routine-row');
|
||
|
||
routineRows.forEach(row => {
|
||
const subjectInput = row.querySelector('input[name="subjects[]"]');
|
||
const teacherSelect = row.querySelector('select[name="teachers[]"]');
|
||
const startTimeInput = row.querySelector('input[name="start_times[]"]');
|
||
const endTimeInput = row.querySelector('input[name="end_times[]"]');
|
||
|
||
if (subjectInput && teacherSelect && startTimeInput && endTimeInput) {
|
||
// Validate all required values are present
|
||
if (!subjectInput.value) {
|
||
showStatus('Subject is required for all rows', true);
|
||
return;
|
||
}
|
||
if (!teacherSelect.value) {
|
||
showStatus('Please select a teacher for all subjects', true);
|
||
return;
|
||
}
|
||
if (!startTimeInput.value) {
|
||
showStatus('Start time is required for all subjects', true);
|
||
return;
|
||
}
|
||
if (!endTimeInput.value) {
|
||
showStatus('End time is required for all subjects', true);
|
||
return;
|
||
}
|
||
|
||
// Validate time format
|
||
if (startTimeInput.value >= endTimeInput.value) {
|
||
showStatus('End time must be after start time', true);
|
||
return;
|
||
}
|
||
|
||
const routine = {
|
||
subject: subjectInput.value,
|
||
teacher: teacherSelect.value,
|
||
start_time: startTimeInput.value,
|
||
end_time: endTimeInput.value,
|
||
course: courseSelect.value,
|
||
period_or_year: periodSelect.value
|
||
};
|
||
|
||
console.log('Adding routine:', routine);
|
||
routines.push(routine);
|
||
}
|
||
});
|
||
|
||
if (routines.length === 0) {
|
||
showStatus('Please assign teachers and times for at least one subject', true);
|
||
return;
|
||
}
|
||
|
||
console.log('Sending routines data:', routines);
|
||
|
||
// Show loading state
|
||
showStatus('Saving routines...');
|
||
|
||
// Send data to server
|
||
fetch('{% url "admin:routine-add-multiple" %}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': '{{ csrf_token }}'
|
||
},
|
||
body: JSON.stringify({ routines: routines })
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('Network response was not ok');
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
console.log('Server response:', data);
|
||
if (data.success) {
|
||
showStatus('Routines saved successfully');
|
||
// Clear form data
|
||
courseSelect.value = '';
|
||
periodSelect.value = '';
|
||
routineTable.querySelector('tbody').innerHTML = '';
|
||
// Redirect after a short delay
|
||
setTimeout(() => {
|
||
window.location.href = '{% url "admin:app_routine_changelist" %}';
|
||
}, 1500);
|
||
} else {
|
||
showStatus('Error saving routines: ' + (data.message || 'Unknown error'), true);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
showStatus('Error saving routines. Please try again.', true);
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
{% endblock %} |