605 lines
18 KiB
PHP
605 lines
18 KiB
PHP
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Laporan Tahunan</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background: #f3f5fb;
|
|
padding: 24px;
|
|
color: #1f2937;
|
|
}
|
|
|
|
/* ACTION BAR */
|
|
.action-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
max-width: 960px;
|
|
margin: 0 auto 16px;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 22px;
|
|
font-weight: 900;
|
|
color: #1e3a8a;
|
|
letter-spacing: -1px;
|
|
}
|
|
.logo-text span { color: #ef4444; }
|
|
|
|
.btn-group { display: flex; gap: 8px; }
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 9px 18px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
border: none;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
|
|
}
|
|
.btn-blue { background: #1d4ed8; color: white; }
|
|
.btn-white { background: white; color: #374151; border: 1px solid #d1d5db; }
|
|
.btn:hover { opacity: .88; }
|
|
|
|
/* CONTAINER */
|
|
.container {
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.07);
|
|
}
|
|
|
|
/* HERO */
|
|
.hero {
|
|
border: 1px solid #dbeafe;
|
|
background: linear-gradient(135deg, #eff6ff, #f7f8ff);
|
|
border-radius: 12px;
|
|
padding: 18px 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.hero-left { display: flex; gap: 16px; align-items: center; }
|
|
|
|
.hero-icon {
|
|
width: 52px;
|
|
height: 52px;
|
|
background: #1d4ed8;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.hero-icon svg { width: 28px; height: 28px; fill: white; }
|
|
|
|
.hero-title { font-size: 18px; font-weight: 900; color: #132b70; line-height: 1.3; }
|
|
.hero-sub { font-size: 11px; color: #6b7280; margin-top: 4px; }
|
|
|
|
.hero-right { font-size: 12px; text-align: right; line-height: 1.8; }
|
|
.hero-right .year { font-size: 22px; font-weight: 800; color: #1e3a8a; }
|
|
|
|
/* SECTION TITLE */
|
|
.section-title {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: #1e3a8a;
|
|
text-transform: uppercase;
|
|
letter-spacing: .6px;
|
|
margin: 20px 0 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.section-title::before {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 4px;
|
|
height: 14px;
|
|
background: #1d4ed8;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* CARDS */
|
|
.cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.card {
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.card-info .card-label { font-size: 11px; color: #6b7280; margin-bottom: 4px; }
|
|
.card-info .card-value { font-size: 28px; font-weight: 800; line-height: 1; }
|
|
.card-info .card-unit { font-size: 11px; color: #9ca3af; margin-top: 4px; }
|
|
.card-icon {
|
|
width: 36px; height: 36px;
|
|
border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.blue { background: #eff6ff; border-color: #bfdbfe; }
|
|
.green { background: #f0fdf4; border-color: #bbf7d0; }
|
|
.yellow { background: #fffbeb; border-color: #fde68a; }
|
|
.purple { background: #faf5ff; border-color: #e9d5ff; }
|
|
|
|
.blue .card-icon { background: #dbeafe; }
|
|
.green .card-icon { background: #dcfce7; }
|
|
.yellow .card-icon { background: #fef3c7; }
|
|
.purple .card-icon { background: #ede9fe; }
|
|
|
|
/* REKAP + DONUT ROW */
|
|
.grid2 {
|
|
display: grid;
|
|
grid-template-columns: 1.4fr 1fr;
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* TABLE */
|
|
.table-box {
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
table { width: 100%; border-collapse: collapse; }
|
|
table thead tr { background: #f8fafc; }
|
|
table th { padding: 10px 12px; font-size: 11px; font-weight: 700; color: #374151; text-align: left; }
|
|
table td { padding: 9px 12px; font-size: 11px; border-top: 1px solid #f1f5f9; }
|
|
table tbody tr:hover { background: #fafafa; }
|
|
|
|
.tfoot-row td { background: #f8fafc; font-weight: 700; border-top: 2px solid #e5e7eb; }
|
|
|
|
.pos { color: #16a34a; font-weight: 600; }
|
|
.net { color: #d97706; font-weight: 600; }
|
|
.neg { color: #dc2626; font-weight: 600; }
|
|
|
|
.badge-green {
|
|
background: #dcfce7;
|
|
color: #15803d;
|
|
border-radius: 20px;
|
|
padding: 2px 8px;
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
}
|
|
|
|
/* DONUT BOX */
|
|
.donut-box {
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.donut-box h4 {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: #1e3a8a;
|
|
text-transform: uppercase;
|
|
letter-spacing: .4px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.donut-wrap {
|
|
position: relative;
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.donut-center {
|
|
position: absolute;
|
|
text-align: center;
|
|
pointer-events: none;
|
|
}
|
|
.donut-center .dc-num { font-size: 20px; font-weight: 800; color: #1e3a8a; }
|
|
.donut-center .dc-label { font-size: 9px; color: #6b7280; }
|
|
|
|
/* TREND BOX */
|
|
.trend-box {
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.trend-box h4 {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: #1e3a8a;
|
|
text-transform: uppercase;
|
|
letter-spacing: .4px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* BOTTOM GRID */
|
|
.bottom-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* KESIMPULAN */
|
|
.kesimpulan-box {
|
|
border: 1px solid #dbeafe;
|
|
background: #eff6ff;
|
|
border-radius: 12px;
|
|
padding: 16px 18px;
|
|
font-size: 12px;
|
|
line-height: 1.7;
|
|
}
|
|
.kesimpulan-box h4 {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: #1e3a8a;
|
|
margin-bottom: 8px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* FOOTER */
|
|
.footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 11px;
|
|
color: #9ca3af;
|
|
padding-top: 14px;
|
|
border-top: 1px solid #f1f5f9;
|
|
}
|
|
|
|
@media print {
|
|
body { background: white; padding: 0; }
|
|
.action-bar { display: none !important; }
|
|
.container { box-shadow: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
{{-- ACTION BAR --}}
|
|
<div class="action-bar no-print" id="actionBar">
|
|
<div></div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-white" onclick="window.print()">🖨 Cetak</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container" id="isiLaporan">
|
|
|
|
{{-- HERO --}}
|
|
<div class="hero">
|
|
<div class="hero-left">
|
|
<div class="hero-icon">
|
|
<svg viewBox="0 0 24 24"><path d="M9 12h6m-6 4h6M7 4H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V6a2 2 0 00-2-2h-2M9 4a2 2 0 002 2h2a2 2 0 002-2M9 4a2 2 0 012-2h2a2 2 0 012 2" stroke="white" stroke-width="2" stroke-linecap="round" fill="none"/></svg>
|
|
</div>
|
|
<div>
|
|
{{-- Logo kecil di atas judul --}}
|
|
<div style="display:inline-flex;align-items:center;gap:5px;background:#1e3a8a;border-radius:6px;padding:3px 10px;margin-bottom:7px;">
|
|
<span style="color:#fff;font-size:11px;font-weight:900;letter-spacing:1px;">-SENTARA-</span>
|
|
<span style="color:#93c5fd;font-size:9px;letter-spacing:2px;">JEMBER</span>
|
|
</div>
|
|
<div class="hero-title">LAPORAN ANALISIS SENTIMEN<br>PER TAHUN</div>
|
|
<div class="hero-sub">SISTEM ANALISIS SENTIMEN WISATA JEMBER</div>
|
|
</div>
|
|
</div>
|
|
<div class="hero-right">
|
|
<div style="color:#6b7280;font-size:11px;">Tahun</div>
|
|
<div class="year">{{ $tahun }}</div>
|
|
<div style="color:#6b7280;font-size:11px;margin-top:6px;">Tanggal Cetak</div>
|
|
<div style="font-weight:600;">{{ now()->format('d M Y H:i') }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- RINGKASAN --}}
|
|
<div class="section-title">Ringkasan Tahunan</div>
|
|
|
|
<div class="cards">
|
|
<div class="card blue">
|
|
<div class="card-info">
|
|
<div class="card-label">Total Ulasan</div>
|
|
<div class="card-value">{{ $totalUlasan }}</div>
|
|
<div class="card-unit">Ulasan</div>
|
|
</div>
|
|
<div class="card-icon">💬</div>
|
|
</div>
|
|
<div class="card green">
|
|
<div class="card-info">
|
|
<div class="card-label">Rata-rata Akurasi</div>
|
|
<div class="card-value" style="font-size:20px;">{{ $akurasi }}%</div>
|
|
<div class="card-unit">Akurasi</div>
|
|
</div>
|
|
<div class="card-icon">✅</div>
|
|
</div>
|
|
<div class="card yellow">
|
|
<div class="card-info">
|
|
<div class="card-label">Total Wisata Dibahas</div>
|
|
<div class="card-value">{{ $totalWisata }}</div>
|
|
<div class="card-unit">Wisata</div>
|
|
</div>
|
|
<div class="card-icon">📍</div>
|
|
</div>
|
|
<div class="card purple">
|
|
<div class="card-info">
|
|
<div class="card-label">Bulan Dianalisis</div>
|
|
<div class="card-value">{{ count($rekapBulanan) }}</div>
|
|
<div class="card-unit">Bulan</div>
|
|
</div>
|
|
<div class="card-icon">📅</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- REKAP + DONUT --}}
|
|
<div class="section-title">Rekap Sentimen per Bulan</div>
|
|
|
|
<div class="grid2">
|
|
<div class="table-box">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Bulan</th>
|
|
<th>Total Ulasan</th>
|
|
<th>Positif</th>
|
|
<th>Netral</th>
|
|
<th>Negatif</th>
|
|
<th>Persentase Positif</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@php $totP=0; $totN=0; $totNt=0; $totAll=0; @endphp
|
|
@foreach($rekapBulanan as $r)
|
|
@php $totP+=$r->positif; $totN+=$r->negatif; $totNt+=$r->netral; $totAll+=$r->total; @endphp
|
|
<tr>
|
|
<td>{{ $r->bulan }}</td>
|
|
<td>{{ $r->total }}</td>
|
|
<td class="pos">{{ $r->positif }}</td>
|
|
<td class="net">{{ $r->netral }}</td>
|
|
<td class="neg">{{ $r->negatif }}</td>
|
|
<td><span class="badge-green">{{ number_format(($r->positif/max($r->total,1))*100,2) }}%</span></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
<tfoot>
|
|
<tr class="tfoot-row">
|
|
<td>TOTAL / RATA-RATA</td>
|
|
<td>{{ $totAll }}</td>
|
|
<td class="pos">{{ $totP }}</td>
|
|
<td class="net">{{ $totNt }}</td>
|
|
<td class="neg">{{ $totN }}</td>
|
|
<td><span class="badge-green">{{ $totAll > 0 ? number_format(($totP/$totAll)*100,2) : 0 }}%</span></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="donut-box">
|
|
<h4>Distribusi Sentimen Tahun {{ $tahun }}</h4>
|
|
<div class="donut-wrap" style="height:220px;">
|
|
<canvas id="donut"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- TREND --}}
|
|
<div class="section-title">Trend Sentimen per Bulan ({{ $tahun }})</div>
|
|
|
|
<div class="trend-box">
|
|
<canvas id="trend" height="90"></canvas>
|
|
</div>
|
|
|
|
{{-- WISATA + KESIMPULAN --}}
|
|
<div class="section-title">Wisata Serpopuler & Kesimpulan</div>
|
|
|
|
<div class="bottom-grid">
|
|
<div class="table-box">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>No</th>
|
|
<th>Wisata</th>
|
|
<th>Total Ulasan</th>
|
|
<th>Persentase Positif</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($wisataPopuler as $i => $w)
|
|
<tr>
|
|
<td>{{ $i+1 }}</td>
|
|
<td>{{ $w->wisata }}</td>
|
|
<td>{{ $w->jumlah }}</td>
|
|
<td><span class="badge-green">{{ $w->persen }}%</span></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="kesimpulan-box">
|
|
<h4>Kesimpulan Tahunan</h4>
|
|
<p>
|
|
Secara umum, sentimen positif mendominasi tahun <b>{{ $tahun }}</b>
|
|
dengan rata-rata <b>{{ $persenPositif }}%</b>.
|
|
@if(isset($wisataPopuler[0]))
|
|
{{ $wisataPopuler[0]->wisata }} menjadi destinasi paling populer
|
|
dengan sentimen positif tertinggi.
|
|
@endif
|
|
Diperlukan peningkatan pada aspek fasilitas, kebersihan, dan pelayanan
|
|
untuk menekan sentimen negatif.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- FOOTER --}}
|
|
<div class="footer">
|
|
<div>SENTARA - Sistem Analisis Sentimen Wisata Jember</div>
|
|
<div>Dicetak oleh: {{ auth()->user()->name }} ({{ ucfirst(auth()->user()->role ?? 'Admin') }})</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// ── DONUT ──
|
|
const donutCtx = document.getElementById('donut');
|
|
const totalUlasan = {{ $totalUlasan > 0 ? $totalUlasan : 1 }};
|
|
const donutData = [{{ $positif }}, {{ $netral }}, {{ $negatif }}];
|
|
|
|
// Plugin: angka total di tengah (tidak numpuk)
|
|
const centerTextPlugin = {
|
|
id: 'centerText',
|
|
beforeDraw(chart) {
|
|
const { ctx, chartArea: { left, top, right, bottom } } = chart;
|
|
const cx = (left + right) / 2;
|
|
const cy = (top + bottom) / 2;
|
|
ctx.save();
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = '#1e3a8a';
|
|
ctx.font = 'bold 22px Arial';
|
|
ctx.fillText(totalUlasan, cx, cy - 8);
|
|
ctx.fillStyle = '#9ca3af';
|
|
ctx.font = '11px Arial';
|
|
ctx.fillText('Total Ulasan', cx, cy + 12);
|
|
ctx.restore();
|
|
}
|
|
};
|
|
|
|
new Chart(donutCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Positif', 'Netral', 'Negatif'],
|
|
datasets: [{
|
|
data: donutData,
|
|
backgroundColor: ['#22c55e', '#f59e0b', '#ef4444'],
|
|
borderWidth: 3,
|
|
borderColor: '#fff',
|
|
hoverOffset: 6,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
cutout: '62%',
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
font: { size: 11 },
|
|
padding: 10,
|
|
generateLabels(chart) {
|
|
const d = chart.data;
|
|
return d.labels.map((lbl, i) => {
|
|
const val = d.datasets[0].data[i];
|
|
const pct = Math.round(val / totalUlasan * 1000) / 10;
|
|
return {
|
|
text: `${lbl} ${val} (${pct}%)`,
|
|
fillStyle: d.datasets[0].backgroundColor[i],
|
|
strokeStyle: '#fff',
|
|
lineWidth: 2,
|
|
index: i,
|
|
};
|
|
});
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label(ctx) {
|
|
const pct = Math.round(ctx.parsed / totalUlasan * 1000) / 10;
|
|
return ` ${ctx.label}: ${ctx.parsed} (${pct}%)`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: [centerTextPlugin]
|
|
});
|
|
|
|
// ── TREND LINE ──
|
|
new Chart(document.getElementById('trend'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: [
|
|
@foreach($rekapBulanan as $r)
|
|
'{{ $r->bulan }}',
|
|
@endforeach
|
|
],
|
|
datasets: [
|
|
{
|
|
label: 'Positif',
|
|
data: [@foreach($rekapBulanan as $r){{ $r->positif }},@endforeach],
|
|
borderColor: '#22c55e',
|
|
backgroundColor: 'rgba(34,197,94,0.08)',
|
|
tension: 0.4, fill: true, pointRadius: 4, pointBackgroundColor: '#22c55e'
|
|
},
|
|
{
|
|
label: 'Netral',
|
|
data: [@foreach($rekapBulanan as $r){{ $r->netral }},@endforeach],
|
|
borderColor: '#f59e0b',
|
|
backgroundColor: 'rgba(245,158,11,0.05)',
|
|
tension: 0.4, fill: false, pointRadius: 4, pointBackgroundColor: '#f59e0b',
|
|
borderDash: [4,3]
|
|
},
|
|
{
|
|
label: 'Negatif',
|
|
data: [@foreach($rekapBulanan as $r){{ $r->negatif }},@endforeach],
|
|
borderColor: '#ef4444',
|
|
backgroundColor: 'rgba(239,68,68,0.05)',
|
|
tension: 0.4, fill: false, pointRadius: 4, pointBackgroundColor: '#ef4444'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
animation: false,
|
|
plugins: {
|
|
legend: { position: 'bottom', labels: { font: { size: 11 }, padding: 12 } }
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true, ticks: { font: { size: 10 } }, grid: { color: '#f1f5f9' } },
|
|
x: { ticks: { font: { size: 10 } }, grid: { display: false } }
|
|
}
|
|
}
|
|
});
|
|
|
|
// ── SIMPAN PDF ──
|
|
function simpanPDF() {
|
|
const el = document.getElementById('isiLaporan');
|
|
const bar = document.getElementById('actionBar');
|
|
bar.style.display = 'none';
|
|
|
|
html2pdf().set({
|
|
margin: 8,
|
|
filename: 'Laporan-Tahunan-{{ $tahun }}.pdf',
|
|
image: { type: 'jpeg', quality: 0.98 },
|
|
html2canvas: { scale: 2, useCORS: true, windowWidth: 960 },
|
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
|
}).from(el).save().then(() => {
|
|
bar.style.display = 'flex';
|
|
});
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html> |