429 lines
19 KiB
HTML
429 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ form.title }} - Submissions{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1><i class="fas fa-list"></i> Form Submissions</h1>
|
|
<p class="text-muted mb-0">{{ form.title }}</p>
|
|
</div>
|
|
<div>
|
|
<a href="{% url 'form_preview' form.id %}" class="btn btn-outline-primary me-2" target="_blank">
|
|
<i class="fas fa-eye"></i> Preview Form
|
|
</a>
|
|
<a href="{% url 'form_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left"></i> Back to Forms
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<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 justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0">{{ page_obj.paginator.count }}</h4>
|
|
<small>Total Submissions</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-users fa-2x opacity-75"></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 justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0">{{ form.submissions.all|length }}</h4>
|
|
<small>All Time</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-chart-line fa-2x opacity-75"></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 justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0">
|
|
{% if form.submissions.first %}
|
|
{{ form.submissions.first.submitted_at|timesince }}
|
|
{% else %}
|
|
No submissions
|
|
{% endif %}
|
|
</h4>
|
|
<small>Latest</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-clock fa-2x opacity-75"></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 justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0">{{ form.created_at|date:"M d" }}</h4>
|
|
<small>Form Created</small>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-calendar fa-2x opacity-75"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Export Options -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0"><i class="fas fa-download"></i> Export Options</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<button class="btn btn-outline-primary me-2" onclick="exportSubmissions('csv')">
|
|
<i class="fas fa-file-csv"></i> Export as CSV
|
|
</button>
|
|
<button class="btn btn-outline-success me-2" onclick="exportSubmissions('excel')">
|
|
<i class="fas fa-file-excel"></i> Export as Excel
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="exportSubmissions('json')">
|
|
<i class="fas fa-file-code"></i> Export as JSON
|
|
</button>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<small class="text-muted">
|
|
Download all submission data for analysis
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submissions List -->
|
|
{% if page_obj %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Recent Submissions</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Submitted</th>
|
|
<th>IP Address</th>
|
|
<th>User Agent</th>
|
|
<th>Data Fields</th>
|
|
<th>Files</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for submission in page_obj %}
|
|
<tr>
|
|
<td>
|
|
<span class="badge bg-primary">{{ submission.id }}</span>
|
|
</td>
|
|
<td>
|
|
<div>
|
|
<strong>{{ submission.submitted_at|date:"M d, Y" }}</strong><br>
|
|
<small class="text-muted">{{ submission.submitted_at|time:"g:i A" }}</small>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<code class="text-muted">{{ submission.ip_address|default:"N/A" }}</code>
|
|
</td>
|
|
<td>
|
|
<small class="text-muted" title="{{ submission.user_agent }}">
|
|
{% if submission.user_agent %}
|
|
{{ submission.user_agent|truncatechars:50 }}
|
|
{% else %}
|
|
N/A
|
|
{% endif %}
|
|
</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info">{{ submission.submission_data.keys|length }} fields</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-success">{{ submission.files.count }} files</span>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary" onclick="viewSubmission({{ submission.id }})" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info" onclick="downloadSubmission({{ submission.id }})" title="Download">
|
|
<i class="fas fa-download"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger" onclick="deleteSubmission({{ submission.id }})" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if page_obj.has_other_pages %}
|
|
<nav aria-label="Submissions pagination" class="mt-4">
|
|
<ul class="pagination justify-content-center">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1">First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
|
</li>
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="card">
|
|
<div class="card-body text-center py-5">
|
|
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
|
<h4>No submissions yet</h4>
|
|
<p class="text-muted">This form hasn't received any submissions yet.</p>
|
|
<a href="{% url 'form_preview' form.id %}" class="btn btn-primary" target="_blank">
|
|
<i class="fas fa-external-link-alt"></i> Test Form
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submission Detail Modal -->
|
|
<div class="modal fade" id="submissionModal" tabindex="-1" aria-labelledby="submissionModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="submissionModalLabel">
|
|
<i class="fas fa-file-alt"></i> Submission Details
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="submissionDetails">
|
|
<!-- Submission details will be loaded here -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" id="downloadSubmissionBtn">
|
|
<i class="fas fa-download"></i> Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
let currentSubmissionId = null;
|
|
|
|
function viewSubmission(submissionId) {
|
|
currentSubmissionId = submissionId;
|
|
|
|
fetch(`/recruitment/api/submissions/${submissionId}/`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
displaySubmissionDetails(data.submission);
|
|
const modal = new bootstrap.Modal(document.getElementById('submissionModal'));
|
|
modal.show();
|
|
} else {
|
|
alert('Error loading submission: ' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error loading submission details');
|
|
});
|
|
}
|
|
|
|
function displaySubmissionDetails(submission) {
|
|
const detailsHtml = `
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Submission Information</h6>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td><strong>ID:</strong></td>
|
|
<td>${submission.id}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Submitted:</strong></td>
|
|
<td>${new Date(submission.submitted_at).toLocaleString()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>IP Address:</strong></td>
|
|
<td>${submission.ip_address || 'N/A'}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Technical Details</h6>
|
|
<table class="table table-sm">
|
|
<tr>
|
|
<td><strong>User Agent:</strong></td>
|
|
<td><small>${submission.user_agent || 'N/A'}</small></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Files:</strong></td>
|
|
<td>${submission.files ? submission.files.length : 0}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Data Fields:</strong></td>
|
|
<td>${Object.keys(submission.submission_data).length}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h6>Submitted Data</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Field</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Object.entries(submission.submission_data).map(([key, value]) => `
|
|
<tr>
|
|
<td><strong>${key}:</strong></td>
|
|
<td>${formatFieldValue(value)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
${submission.files && submission.files.length > 0 ? `
|
|
<hr>
|
|
<h6>Uploaded Files</h6>
|
|
<div class="list-group">
|
|
${submission.files.map(file => `
|
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<i class="fas fa-file me-2"></i>
|
|
<strong>${file.original_filename}</strong>
|
|
<br>
|
|
<small class="text-muted">
|
|
${file.file_size ? formatFileSize(file.file_size) : 'Unknown size'} •
|
|
Uploaded ${new Date(file.uploaded_at).toLocaleString()}
|
|
</small>
|
|
</div>
|
|
<a href="${file.file_url}" class="btn btn-sm btn-outline-primary" target="_blank">
|
|
<i class="fas fa-download"></i> Download
|
|
</a>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
` : ''}
|
|
`;
|
|
|
|
document.getElementById('submissionDetails').innerHTML = detailsHtml;
|
|
document.getElementById('downloadSubmissionBtn').onclick = () => downloadSubmission(submission.id);
|
|
}
|
|
|
|
function formatFieldValue(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.join(', ');
|
|
}
|
|
if (typeof value === 'object' && value !== null) {
|
|
return JSON.stringify(value);
|
|
}
|
|
return value || 'N/A';
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (!bytes) return 'Unknown';
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
function downloadSubmission(submissionId) {
|
|
window.open(`/recruitment/api/submissions/${submissionId}/download/`, '_blank');
|
|
}
|
|
|
|
function deleteSubmission(submissionId) {
|
|
if (confirm('Are you sure you want to delete this submission? This action cannot be undone.')) {
|
|
fetch(`/recruitment/api/submissions/${submissionId}/delete/`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error deleting submission: ' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error deleting submission');
|
|
});
|
|
}
|
|
}
|
|
|
|
function exportSubmissions(format) {
|
|
window.open(`/recruitment/api/forms/{{ form.id }}/export/?format=${format}`, '_blank');
|
|
}
|
|
|
|
function getCsrfToken() {
|
|
const cookie = document.cookie.split(';').find(c => c.trim().startsWith('csrftoken='));
|
|
return cookie ? cookie.split('=')[1] : '';
|
|
}
|
|
</script>
|
|
{% endblock %}
|