develop #1
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
111
static/js/export-pdf.js
Normal file
111
static/js/export-pdf.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Export dashboard as PDF by capturing a screenshot of #dashboard-content
|
||||
* and assembling it into a multi-page PDF (html2canvas + jsPDF).
|
||||
*/
|
||||
function exportDashboardToPdf() {
|
||||
var el = document.getElementById('dashboard-content');
|
||||
if (!el) {
|
||||
if (typeof console !== 'undefined' && console.warn) {
|
||||
console.warn('export-pdf: #dashboard-content not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var btn = document.getElementById('pdf-export-btn');
|
||||
var originalText = btn ? btn.innerHTML : '';
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = 'Generating PDF…';
|
||||
}
|
||||
|
||||
var regionEl = document.getElementById('regionBadge');
|
||||
var region = regionEl ? (regionEl.textContent || '').trim() : '';
|
||||
|
||||
if (typeof html2canvas === 'undefined' || typeof jspdf === 'undefined') {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
alert('PDF export requires html2canvas and jsPDF. Please refresh the page.');
|
||||
return;
|
||||
}
|
||||
|
||||
var JsPDFConstructor = (typeof jspdf !== 'undefined' && jspdf.jsPDF) ? jspdf.jsPDF : jspdf;
|
||||
|
||||
var auditSection = el.querySelector('section[aria-label="Audit analysis"]');
|
||||
var auditSectionDisplay = '';
|
||||
if (auditSection) {
|
||||
auditSectionDisplay = auditSection.style.display;
|
||||
auditSection.style.display = 'none';
|
||||
}
|
||||
|
||||
function restoreAuditSection() {
|
||||
if (auditSection) {
|
||||
auditSection.style.display = auditSectionDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
html2canvas(el, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
logging: false
|
||||
}).then(function(canvas) {
|
||||
var imgW = canvas.width;
|
||||
var imgH = canvas.height;
|
||||
var dataUrl = canvas.toDataURL('image/png');
|
||||
|
||||
var doc = new JsPDFConstructor({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
||||
var pageW = 210;
|
||||
var pageH = 297;
|
||||
var margin = 10;
|
||||
var contentW = pageW - 2 * margin;
|
||||
var headerH = 14;
|
||||
var firstPageImgTop = margin + headerH;
|
||||
var firstPageImgH = pageH - firstPageImgTop - margin;
|
||||
var otherPageImgH = pageH - 2 * margin;
|
||||
|
||||
var imgWmm = contentW;
|
||||
var imgHmm = contentW * (imgH / imgW);
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.text('Dashboard report', margin, margin + 6);
|
||||
doc.setFontSize(10);
|
||||
doc.text(region ? 'Region: ' + region : '', margin, margin + 12);
|
||||
|
||||
var shown = 0;
|
||||
var totalH = imgHmm;
|
||||
var pageNum = 0;
|
||||
var imgYmm = firstPageImgTop;
|
||||
|
||||
while (shown < totalH) {
|
||||
if (pageNum > 0) {
|
||||
doc.addPage();
|
||||
imgYmm = margin;
|
||||
}
|
||||
var sliceH = pageNum === 0 ? firstPageImgH : otherPageImgH;
|
||||
var yOffset = -shown;
|
||||
doc.addImage(dataUrl, 'PNG', margin, imgYmm + yOffset, imgWmm, imgHmm);
|
||||
shown += sliceH;
|
||||
pageNum += 1;
|
||||
}
|
||||
|
||||
doc.save('dashboard-report.pdf');
|
||||
|
||||
restoreAuditSection();
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
}).catch(function (err) {
|
||||
if (typeof console !== 'undefined' && console.error) {
|
||||
console.error('export-pdf:', err);
|
||||
}
|
||||
restoreAuditSection();
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
});
|
||||
}
|
||||
10
static/js/html2canvas-pro.min.js
vendored
Normal file
10
static/js/html2canvas-pro.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
397
static/js/jspdf.umd.min.js
vendored
Normal file
397
static/js/jspdf.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -5,10 +5,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}SWatcher{% endblock %}</title>
|
||||
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'css/output.css' %}">
|
||||
<script src="{% static 'js/html2canvas-pro.min.js' %}"></script>
|
||||
<script src="{% static 'js/jspdf.umd.min.js' %}"></script>
|
||||
{% block imports %}
|
||||
{% endblock %}
|
||||
{% block css %}
|
||||
@@ -22,7 +25,7 @@
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="px-1 flex items-center gap-3 pr-10">
|
||||
<button type="button" class="btn btn-ghost btn-sm no-print" onclick="window.print()" title="Save as PDF" aria-label="Save as PDF">
|
||||
<button type="button" id="pdf-export-btn" class="btn btn-ghost btn-sm no-print" onclick="exportDashboardToPdf()" title="Save as PDF" aria-label="Save as PDF">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
@@ -70,6 +73,7 @@
|
||||
localStorage.setItem('theme', newTheme);
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/export-pdf.js' %}"></script>
|
||||
{% block script %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
{% block content %}
|
||||
<!-- MAIN DASHBOARD -->
|
||||
<div class="p-4 space-y-8" {% if skeleton %}data-dashboard="skeleton"{% endif %}>
|
||||
<div id="dashboard-content" class="p-4 space-y-8" {% if skeleton %}data-dashboard="skeleton"{% endif %}>
|
||||
<!-- QUICK STATS ROW -->
|
||||
<section class="grid grid-cols-1 lg:grid-cols-3 gap-4" aria-label="Quick stats">
|
||||
<!-- CPU Utilization -->
|
||||
|
||||
@@ -14,10 +14,13 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'favicon.ico', permanent=False)),
|
||||
path('', include('dashboard.urls')),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user