laporan tahunan
This commit is contained in:
parent
80057c1acc
commit
6f6eabcaef
|
|
@ -19,95 +19,432 @@ public function downloadBulanan($periode)
|
|||
|
||||
$data = $this->collectData($periode);
|
||||
|
||||
$data['periode'] = $periodeData;
|
||||
$data['judulLaporan'] = 'Laporan Analisis Sentimen - ' . ($periodeData->nama ?? '-');
|
||||
$data['evaluasi'] = DB::table('evaluasi_model')->orderByDesc('id')->first()
|
||||
?? (object)['accuracy' => 0];
|
||||
$data['periode'] = $periodeData;
|
||||
|
||||
// Ulasan KOTOR — dari tabel ulasan mentah
|
||||
$data['totalUlasanKotor'] = DB::table('ulasan')
|
||||
$data['judulLaporan'] =
|
||||
'Laporan Analisis Sentimen - ' .
|
||||
($periodeData->nama ?? '-');
|
||||
|
||||
$data['evaluasi'] =
|
||||
DB::table('evaluasi_model')
|
||||
->orderByDesc('id')
|
||||
->first()
|
||||
?? (object)['accuracy' => 0];
|
||||
|
||||
// TOTAL ULASAN KOTOR
|
||||
$data['totalUlasanKotor'] =
|
||||
DB::table('ulasan')
|
||||
->where('periode_id', $periode)
|
||||
->count();
|
||||
|
||||
// Trend by tanggal ULASAN (bukan created_at analisis)
|
||||
$data['trendHarian'] = DB::table('hasil_analisis')
|
||||
// TREND HARIAN
|
||||
$data['trendHarian'] =
|
||||
DB::table('hasil_analisis')
|
||||
->select(
|
||||
DB::raw('DATE(created_at) as tanggal'), // ✅ ganti tanggal → created_at
|
||||
DB::raw("SUM(CASE WHEN sentimen='positif' THEN 1 ELSE 0 END) as positif"),
|
||||
DB::raw("SUM(CASE WHEN sentimen='netral' THEN 1 ELSE 0 END) as netral"),
|
||||
DB::raw("SUM(CASE WHEN sentimen='negatif' THEN 1 ELSE 0 END) as negatif")
|
||||
DB::raw('DATE(created_at) as tanggal'),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='positif'
|
||||
THEN 1 ELSE 0 END
|
||||
) as positif
|
||||
"),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='netral'
|
||||
THEN 1 ELSE 0 END
|
||||
) as netral
|
||||
"),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='negatif'
|
||||
THEN 1 ELSE 0 END
|
||||
) as negatif
|
||||
")
|
||||
)
|
||||
->where('periode_id', $periode)
|
||||
->groupBy(DB::raw('DATE(created_at)')) // ✅ ganti tanggal → created_at
|
||||
->orderBy(DB::raw('DATE(created_at)')) // ✅ ganti tanggal → created_at
|
||||
->groupBy(DB::raw('DATE(created_at)'))
|
||||
->orderBy(DB::raw('DATE(created_at)'))
|
||||
->get();
|
||||
|
||||
return view('laporan.bulanan', $data);
|
||||
return view(
|
||||
'laporan.bulanan',
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function downloadTahunan(Request $request)
|
||||
{
|
||||
$tahun = $request->tahun;
|
||||
|
||||
$periodeList = DB::table('periode_analisis')
|
||||
$periodeList =
|
||||
DB::table('periode_analisis')
|
||||
->where('tahun', $tahun)
|
||||
->orderBy('bulan')
|
||||
->get();
|
||||
|
||||
if ($periodeList->isEmpty()) {
|
||||
return back()->with('error', 'Tidak ada data tahun tersebut');
|
||||
|
||||
return back()->with(
|
||||
'error',
|
||||
'Tidak ada data tahun tersebut'
|
||||
);
|
||||
}
|
||||
|
||||
return view('laporan.tahunan', compact('tahun', 'periodeList'));
|
||||
$periodeIds =
|
||||
$periodeList->pluck('id');
|
||||
|
||||
// TOTAL ULASAN
|
||||
$totalUlasan =
|
||||
DB::table('hasil_analisis')
|
||||
->whereIn(
|
||||
'periode_id',
|
||||
$periodeIds
|
||||
)
|
||||
->count();
|
||||
|
||||
// TOTAL SENTIMEN
|
||||
$positif =
|
||||
DB::table('hasil_analisis')
|
||||
->whereIn(
|
||||
'periode_id',
|
||||
$periodeIds
|
||||
)
|
||||
->where(
|
||||
'sentimen',
|
||||
'positif'
|
||||
)
|
||||
->count();
|
||||
|
||||
$netral =
|
||||
DB::table('hasil_analisis')
|
||||
->whereIn(
|
||||
'periode_id',
|
||||
$periodeIds
|
||||
)
|
||||
->where(
|
||||
'sentimen',
|
||||
'netral'
|
||||
)
|
||||
->count();
|
||||
|
||||
$negatif =
|
||||
DB::table('hasil_analisis')
|
||||
->whereIn(
|
||||
'periode_id',
|
||||
$periodeIds
|
||||
)
|
||||
->where(
|
||||
'sentimen',
|
||||
'negatif'
|
||||
)
|
||||
->count();
|
||||
|
||||
// PERSEN POSITIF
|
||||
$persenPositif =
|
||||
$totalUlasan > 0
|
||||
? round(
|
||||
($positif / $totalUlasan) * 100,
|
||||
2
|
||||
)
|
||||
: 0;
|
||||
|
||||
// AKURASI
|
||||
$akurasi =
|
||||
DB::table('evaluasi_model')
|
||||
->latest('id')
|
||||
->value('accuracy')
|
||||
?? 0;
|
||||
|
||||
// TOTAL WISATA
|
||||
$totalWisata =
|
||||
DB::table('hasil_analisis')
|
||||
->whereIn(
|
||||
'periode_id',
|
||||
$periodeIds
|
||||
)
|
||||
->distinct()
|
||||
->count('wisata');
|
||||
|
||||
// REKAP BULANAN
|
||||
$rekapBulanan =
|
||||
DB::table('hasil_analisis as h')
|
||||
->join(
|
||||
'periode_analisis as p',
|
||||
'h.periode_id',
|
||||
'=',
|
||||
'p.id'
|
||||
)
|
||||
|
||||
->select(
|
||||
|
||||
'p.nama as bulan',
|
||||
|
||||
DB::raw(
|
||||
'COUNT(*) as total'
|
||||
),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='positif'
|
||||
THEN 1 ELSE 0 END
|
||||
) as positif
|
||||
"),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='netral'
|
||||
THEN 1 ELSE 0 END
|
||||
) as netral
|
||||
"),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='negatif'
|
||||
THEN 1 ELSE 0 END
|
||||
) as negatif
|
||||
")
|
||||
)
|
||||
|
||||
->whereIn(
|
||||
'periode_id',
|
||||
$periodeIds
|
||||
)
|
||||
|
||||
->groupBy(
|
||||
'p.nama',
|
||||
'p.bulan'
|
||||
)
|
||||
|
||||
->orderBy(
|
||||
'p.bulan'
|
||||
)
|
||||
|
||||
->get();
|
||||
|
||||
// WISATA POPULER
|
||||
$wisataPopuler =
|
||||
DB::table('hasil_analisis')
|
||||
|
||||
->select(
|
||||
|
||||
'wisata',
|
||||
|
||||
DB::raw(
|
||||
'COUNT(*) as jumlah'
|
||||
),
|
||||
|
||||
DB::raw("
|
||||
ROUND(
|
||||
(
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='positif'
|
||||
THEN 1 ELSE 0 END
|
||||
)
|
||||
/
|
||||
COUNT(*)
|
||||
) * 100
|
||||
,2)
|
||||
as persen
|
||||
")
|
||||
)
|
||||
|
||||
->whereIn(
|
||||
'periode_id',
|
||||
$periodeIds
|
||||
)
|
||||
|
||||
->groupBy(
|
||||
'wisata'
|
||||
)
|
||||
|
||||
->orderByDesc(
|
||||
'persen'
|
||||
)
|
||||
|
||||
->limit(5)
|
||||
|
||||
->get();
|
||||
|
||||
return view(
|
||||
|
||||
'laporan.tahunan',
|
||||
|
||||
compact(
|
||||
|
||||
'tahun',
|
||||
'periodeList',
|
||||
'totalUlasan',
|
||||
'akurasi',
|
||||
'totalWisata',
|
||||
'rekapBulanan',
|
||||
'wisataPopuler',
|
||||
'positif',
|
||||
'netral',
|
||||
'negatif',
|
||||
'persenPositif'
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function collectData($periodeId)
|
||||
{
|
||||
$totalUlasan = DB::table('hasil_analisis')
|
||||
->where('periode_id', $periodeId)
|
||||
$totalUlasan =
|
||||
DB::table('hasil_analisis')
|
||||
->where(
|
||||
'periode_id',
|
||||
$periodeId
|
||||
)
|
||||
->count();
|
||||
|
||||
$totalPositif = DB::table('hasil_analisis')
|
||||
->where('periode_id', $periodeId)
|
||||
->where('sentimen', 'positif')
|
||||
$totalPositif =
|
||||
DB::table('hasil_analisis')
|
||||
->where(
|
||||
'periode_id',
|
||||
$periodeId
|
||||
)
|
||||
->where(
|
||||
'sentimen',
|
||||
'positif'
|
||||
)
|
||||
->count();
|
||||
|
||||
$totalNetral = DB::table('hasil_analisis')
|
||||
->where('periode_id', $periodeId)
|
||||
->where('sentimen', 'netral')
|
||||
$totalNetral =
|
||||
DB::table('hasil_analisis')
|
||||
->where(
|
||||
'periode_id',
|
||||
$periodeId
|
||||
)
|
||||
->where(
|
||||
'sentimen',
|
||||
'netral'
|
||||
)
|
||||
->count();
|
||||
|
||||
$totalNegatif = DB::table('hasil_analisis')
|
||||
->where('periode_id', $periodeId)
|
||||
->where('sentimen', 'negatif')
|
||||
$totalNegatif =
|
||||
DB::table('hasil_analisis')
|
||||
->where(
|
||||
'periode_id',
|
||||
$periodeId
|
||||
)
|
||||
->where(
|
||||
'sentimen',
|
||||
'negatif'
|
||||
)
|
||||
->count();
|
||||
|
||||
$persen = [
|
||||
'positif' => $totalUlasan ? round(($totalPositif / $totalUlasan) * 100, 2) : 0,
|
||||
'netral' => $totalUlasan ? round(($totalNetral / $totalUlasan) * 100, 2) : 0,
|
||||
'negatif' => $totalUlasan ? round(($totalNegatif / $totalUlasan) * 100, 2) : 0,
|
||||
|
||||
'positif' =>
|
||||
$totalUlasan
|
||||
? round(
|
||||
($totalPositif /
|
||||
$totalUlasan) * 100,
|
||||
2
|
||||
)
|
||||
: 0,
|
||||
|
||||
'netral' =>
|
||||
$totalUlasan
|
||||
? round(
|
||||
($totalNetral /
|
||||
$totalUlasan) * 100,
|
||||
2
|
||||
)
|
||||
: 0,
|
||||
|
||||
'negatif' =>
|
||||
$totalUlasan
|
||||
? round(
|
||||
($totalNegatif /
|
||||
$totalUlasan) * 100,
|
||||
2
|
||||
)
|
||||
: 0,
|
||||
];
|
||||
|
||||
$perWisata = DB::table('hasil_analisis')
|
||||
$perWisata =
|
||||
DB::table('hasil_analisis')
|
||||
|
||||
->select(
|
||||
|
||||
'wisata',
|
||||
DB::raw('COUNT(*) as total'),
|
||||
DB::raw("SUM(CASE WHEN sentimen='positif' THEN 1 ELSE 0 END) as positif"),
|
||||
DB::raw("SUM(CASE WHEN sentimen='netral' THEN 1 ELSE 0 END) as netral"),
|
||||
DB::raw("SUM(CASE WHEN sentimen='negatif' THEN 1 ELSE 0 END) as negatif")
|
||||
|
||||
DB::raw(
|
||||
'COUNT(*) as total'
|
||||
),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='positif'
|
||||
THEN 1 ELSE 0 END
|
||||
) as positif
|
||||
"),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='netral'
|
||||
THEN 1 ELSE 0 END
|
||||
) as netral
|
||||
"),
|
||||
|
||||
DB::raw("
|
||||
SUM(
|
||||
CASE
|
||||
WHEN sentimen='negatif'
|
||||
THEN 1 ELSE 0 END
|
||||
) as negatif
|
||||
")
|
||||
)
|
||||
->where('periode_id', $periodeId)
|
||||
->groupBy('wisata')
|
||||
->orderByDesc('positif')
|
||||
|
||||
->where(
|
||||
'periode_id',
|
||||
$periodeId
|
||||
)
|
||||
|
||||
->groupBy(
|
||||
'wisata'
|
||||
)
|
||||
|
||||
->orderByDesc(
|
||||
'positif'
|
||||
)
|
||||
|
||||
->get();
|
||||
|
||||
return [
|
||||
'totalUlasan' => $totalUlasan,
|
||||
'totalPositif' => $totalPositif,
|
||||
'totalNetral' => $totalNetral,
|
||||
'totalNegatif' => $totalNegatif,
|
||||
'persen' => $persen,
|
||||
'perWisata' => $perWisata,
|
||||
|
||||
'totalUlasan' =>
|
||||
$totalUlasan,
|
||||
|
||||
'totalPositif' =>
|
||||
$totalPositif,
|
||||
|
||||
'totalNetral' =>
|
||||
$totalNetral,
|
||||
|
||||
'totalNegatif' =>
|
||||
$totalNegatif,
|
||||
|
||||
'persen' =>
|
||||
$persen,
|
||||
|
||||
'perWisata' =>
|
||||
$perWisata,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +1,605 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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; }
|
||||
|
||||
<title>
|
||||
Laporan Tahunan
|
||||
</title>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f3f5fb;
|
||||
padding: 24px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
<style>
|
||||
/* ACTION BAR */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 960px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
body{
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
color: #222;
|
||||
}
|
||||
.logo-text {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
color: #1e3a8a;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
.logo-text span { color: #ef4444; }
|
||||
|
||||
h1,h2,h3{
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.btn-group { display: flex; gap: 8px; }
|
||||
|
||||
.title{
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.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; }
|
||||
|
||||
table{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
/* 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);
|
||||
}
|
||||
|
||||
table th{
|
||||
background: #f3f3f3;
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td{
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
}
|
||||
.hero-left { display: flex; gap: 16px; align-items: center; }
|
||||
|
||||
</style>
|
||||
.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>
|
||||
|
||||
<div class="title">
|
||||
{{-- 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>
|
||||
|
||||
<h1>
|
||||
LAPORAN TAHUNAN ANALISIS SENTIMEN
|
||||
</h1>
|
||||
|
||||
<h2>
|
||||
Wisata Kabupaten Jember
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
Tahun:
|
||||
<strong>
|
||||
{{ $tahun }}
|
||||
</strong>
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<table>
|
||||
{{-- RINGKASAN --}}
|
||||
<div class="section-title">Ringkasan Tahunan</div>
|
||||
|
||||
<tr>
|
||||
<th>Total Ulasan</th>
|
||||
<td>{{ $totalUlasan ?? 0 }}</td>
|
||||
</tr>
|
||||
<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>
|
||||
|
||||
<tr>
|
||||
<th>Positif</th>
|
||||
<td>{{ $positif ?? 0 }}</td>
|
||||
</tr>
|
||||
{{-- REKAP + DONUT --}}
|
||||
<div class="section-title">Rekap Sentimen per Bulan</div>
|
||||
|
||||
<tr>
|
||||
<th>Negatif</th>
|
||||
<td>{{ $negatif ?? 0 }}</td>
|
||||
</tr>
|
||||
<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>
|
||||
|
||||
<tr>
|
||||
<th>Netral</th>
|
||||
<td>{{ $netral ?? 0 }}</td>
|
||||
</tr>
|
||||
<div class="donut-box">
|
||||
<h4>Distribusi Sentimen Tahun {{ $tahun }}</h4>
|
||||
<div class="donut-wrap" style="height:220px;">
|
||||
<canvas id="donut"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</table>
|
||||
{{-- TREND --}}
|
||||
<div class="section-title">Trend Sentimen per Bulan ({{ $tahun }})</div>
|
||||
|
||||
<br>
|
||||
<div class="trend-box">
|
||||
<canvas id="trend" height="90"></canvas>
|
||||
</div>
|
||||
|
||||
<h3>Rekomendasi Tahunan</h3>
|
||||
{{-- WISATA + KESIMPULAN --}}
|
||||
<div class="section-title">Wisata Serpopuler & Kesimpulan</div>
|
||||
|
||||
@forelse(($rekomendasi ?? []) as $item)
|
||||
<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>
|
||||
|
||||
<p>
|
||||
• {{ $item->rekomendasi }}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
@empty
|
||||
{{-- 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>
|
||||
|
||||
<p>
|
||||
Tidak ada rekomendasi.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@endforelse
|
||||
<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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php $__env->startSection('title', __('Not Found')); ?>
|
||||
<?php $__env->startSection('code', '404'); ?>
|
||||
<?php $__env->startSection('message', __('Not Found')); ?>
|
||||
|
||||
<?php echo $__env->make('errors::minimal', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\vendor\laravel\framework\src\Illuminate\Foundation\Exceptions/views/404.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -1,377 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?php echo e($judulLaporan); ?></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; }
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f1f5f9;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* ── Sticky action bar pojok kanan atas ── */
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 24px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.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 8px rgba(0,0,0,0.15);
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.btn:hover { opacity: .85; }
|
||||
.btn-primary { background: #1d4ed8; color: white; }
|
||||
.btn-secondary { background: white; color: #374151; border: 1px solid #d1d5db; }
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* LOGO */
|
||||
.logo { font-size: 26px; font-weight: 800; color: #1e3a8a; margin-bottom: 20px; }
|
||||
|
||||
/* HERO */
|
||||
.hero {
|
||||
border: 1px solid #dbeafe;
|
||||
background: linear-gradient(135deg, #eff6ff, #f7f8ff);
|
||||
border-radius: 12px;
|
||||
padding: 22px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.hero-title { font-size: 22px; font-weight: 800; color: #132b70; line-height: 1.3; }
|
||||
.hero-sub { margin-top: 6px; color: #374151; font-size: 13px; }
|
||||
|
||||
.hero-right {
|
||||
min-width: 160px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* SECTION TITLE */
|
||||
.section-title {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: #1e3a8a;
|
||||
letter-spacing: .5px;
|
||||
margin: 24px 0 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* CARDS */
|
||||
.cards { display: flex; gap: 12px; margin-bottom: 24px; }
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card .card-label { font-size: 12px; color: #6b7280; margin-bottom: 6px; }
|
||||
.card .card-value { font-size: 26px; font-weight: 800; margin: 0; }
|
||||
.card .card-unit { font-size: 12px; color: #9ca3af; margin-top: 4px; }
|
||||
|
||||
.blue { background: #eff6ff; border-color: #bfdbfe; }
|
||||
.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.yellow { background: #fffbeb; border-color: #fde68a; }
|
||||
.purple { background: #faf5ff; border-color: #e9d5ff; }
|
||||
|
||||
/* CHARTS */
|
||||
.chart-row { display: flex; gap: 16px; margin-bottom: 24px; align-items: flex-start; }
|
||||
|
||||
.box-pie {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.box-bar {
|
||||
flex: 1;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.box-pie h4, .box-bar h4 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
color: #1e3a8a;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* TABLE */
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
|
||||
table th { background: #f8fafc; font-size: 12px; font-weight: 600; }
|
||||
table th, table td { border: 1px solid #e5e7eb; padding: 9px 11px; font-size: 12px; }
|
||||
|
||||
.badge {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
border-radius: 20px;
|
||||
padding: 3px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* KESIMPULAN */
|
||||
.kesimpulan {
|
||||
border: 1px solid #dbeafe;
|
||||
background: #eff6ff;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
.footer {
|
||||
margin-top: 24px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Sembunyikan action bar saat print */
|
||||
@media print {
|
||||
body { background: white; padding: 0; }
|
||||
.action-bar { display: none !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<div class="action-bar" id="actionBar">
|
||||
<button class="btn btn-secondary" onclick="cetakHalaman()">🖨️ Cetak</button>
|
||||
</div>
|
||||
|
||||
<div class="container" id="isiLaporan">
|
||||
|
||||
|
||||
<div class="logo">SENTARA</div>
|
||||
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-left">
|
||||
<div class="hero-title">LAPORAN ANALISIS SENTIMEN<br>PER BULAN</div>
|
||||
<div class="hero-sub">SISTEM ANALISIS SENTIMEN WISATA JEMBER</div>
|
||||
</div>
|
||||
<div class="hero-right">
|
||||
<b>Periode</b><br>
|
||||
<?php echo e($periode->nama ?? '-'); ?><br><br>
|
||||
<b>Tanggal Cetak</b><br>
|
||||
<?php echo e(now()->format('d M Y H:i')); ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section-title">Ringkasan</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card blue">
|
||||
<div class="card-label">Total Ulasan</div>
|
||||
<p class="card-value"><?php echo e($totalUlasanKotor); ?></p>
|
||||
<div class="card-unit">Ulasan Masuk</div>
|
||||
</div>
|
||||
<div class="card green">
|
||||
<div class="card-label">Hasil Analisis</div>
|
||||
<p class="card-value"><?php echo e($totalUlasan); ?></p>
|
||||
<div class="card-unit">Data Bersih</div>
|
||||
</div>
|
||||
<div class="card yellow">
|
||||
<div class="card-label">Wisata Dibahas</div>
|
||||
<p class="card-value"><?php echo e(count($perWisata)); ?></p>
|
||||
<div class="card-unit">Destinasi</div>
|
||||
</div>
|
||||
<div class="card purple">
|
||||
<div class="card-label">Akurasi Analisis</div>
|
||||
<p class="card-value"><?php echo e(number_format(($evaluasi->accuracy ?? 0) * 100, 2)); ?>%</p>
|
||||
<div class="card-unit">Model</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section-title">Visualisasi</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="box-pie">
|
||||
<h4>Distribusi Sentimen</h4>
|
||||
<canvas id="pieChart" width="220" height="220"></canvas>
|
||||
</div>
|
||||
<div class="box-bar">
|
||||
<h4>Sentimen per Destinasi</h4>
|
||||
<canvas id="barChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section-title">Top Wisata Berdasarkan Sentimen Positif</div>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Wisata</th>
|
||||
<th>Total</th>
|
||||
<th>Positif</th>
|
||||
<th>Netral</th>
|
||||
<th>Negatif</th>
|
||||
<th>% Positif</th>
|
||||
</tr>
|
||||
<?php $__currentLoopData = $perWisata; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $i => $w): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td><?php echo e($i + 1); ?></td>
|
||||
<td><?php echo e($w->wisata); ?></td>
|
||||
<td><?php echo e($w->total); ?></td>
|
||||
<td><?php echo e($w->positif); ?></td>
|
||||
<td><?php echo e($w->netral); ?></td>
|
||||
<td><?php echo e($w->negatif); ?></td>
|
||||
<td><span class="badge"><?php echo e($w->total > 0 ? round(($w->positif / $w->total) * 100, 2) : 0); ?>%</span></td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="kesimpulan">
|
||||
<b>KESIMPULAN</b>
|
||||
<p style="margin:8px 0 0">
|
||||
Pada periode <b><?php echo e($periode->nama ?? ''); ?></b>,
|
||||
terdapat <b><?php echo e($totalUlasanKotor); ?></b> ulasan masuk dengan
|
||||
<b><?php echo e($totalUlasan); ?></b> data berhasil dianalisis.
|
||||
Sentimen positif mendominasi dengan persentase <b><?php echo e($persen['positif'] ?? 0); ?>%</b>,
|
||||
diikuti netral <b><?php echo e($persen['netral'] ?? 0); ?>%</b>
|
||||
dan negatif <b><?php echo e($persen['negatif'] ?? 0); ?>%</b>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="footer">
|
||||
<div>SENTARA — Sistem Analisis Sentimen Wisata Jember</div>
|
||||
<div>Dicetak oleh: Admin | <?php echo e(now()->format('d M Y H:i')); ?></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── PIE / DOUGHNUT CHART ──
|
||||
new Chart(document.getElementById('pieChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Positif', 'Netral', 'Negatif'],
|
||||
datasets: [{
|
||||
data: [<?php echo e($totalPositif); ?>, <?php echo e($totalNetral); ?>, <?php echo e($totalNegatif); ?>],
|
||||
backgroundColor: ['#22c55e', '#f59e0b', '#ef4444'],
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom', labels: { font: { size: 11 }, padding: 10 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── BAR CHART SENTIMEN PER DESTINASI ──
|
||||
const wisataLabels = <?php echo json_encode($perWisata->pluck('wisata'), 15, 512) ?>;
|
||||
const wisataPos = <?php echo json_encode($perWisata->pluck('positif'), 15, 512) ?>;
|
||||
const wisataNet = <?php echo json_encode($perWisata->pluck('netral'), 15, 512) ?>;
|
||||
const wisataNeg = <?php echo json_encode($perWisata->pluck('negatif'), 15, 512) ?>;
|
||||
|
||||
new Chart(document.getElementById('barChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: wisataLabels,
|
||||
datasets: [
|
||||
{ label: 'Positif', data: wisataPos, backgroundColor: '#22c55e' },
|
||||
{ label: 'Netral', data: wisataNet, backgroundColor: '#f59e0b' },
|
||||
{ label: 'Negatif', data: wisataNeg, backgroundColor: '#ef4444' }
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom', labels: { font: { size: 11 } } }
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, ticks: { font: { size: 11 } } },
|
||||
x: { ticks: { font: { size: 10 } } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── CETAK ──
|
||||
function cetakHalaman() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
// ── SIMPAN PDF ──
|
||||
function simpanPDF() {
|
||||
const el = document.getElementById('isiLaporan');
|
||||
const bar = document.getElementById('actionBar');
|
||||
bar.style.display = 'none';
|
||||
|
||||
html2pdf().set({
|
||||
margin: 10,
|
||||
filename: 'Laporan-<?php echo e(Str::slug($periode->nama ?? "bulanan")); ?>.pdf',
|
||||
image: { type: 'jpeg', quality: 0.98 },
|
||||
html2canvas: { scale: 2, useCORS: true },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||
}).from(el).save().then(() => {
|
||||
bar.style.display = 'flex';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/laporan/bulanan.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -0,0 +1,605 @@
|
|||
<!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>
|
||||
|
||||
|
||||
<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">
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
<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"><?php echo e($tahun); ?></div>
|
||||
<div style="color:#6b7280;font-size:11px;margin-top:6px;">Tanggal Cetak</div>
|
||||
<div style="font-weight:600;"><?php echo e(now()->format('d M Y H:i')); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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"><?php echo e($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;"><?php echo e($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"><?php echo e($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"><?php echo e(count($rekapBulanan)); ?></div>
|
||||
<div class="card-unit">Bulan</div>
|
||||
</div>
|
||||
<div class="card-icon">📅</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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; ?>
|
||||
<?php $__currentLoopData = $rekapBulanan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $r): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php $totP+=$r->positif; $totN+=$r->negatif; $totNt+=$r->netral; $totAll+=$r->total; ?>
|
||||
<tr>
|
||||
<td><?php echo e($r->bulan); ?></td>
|
||||
<td><?php echo e($r->total); ?></td>
|
||||
<td class="pos"><?php echo e($r->positif); ?></td>
|
||||
<td class="net"><?php echo e($r->netral); ?></td>
|
||||
<td class="neg"><?php echo e($r->negatif); ?></td>
|
||||
<td><span class="badge-green"><?php echo e(number_format(($r->positif/max($r->total,1))*100,2)); ?>%</span></td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="tfoot-row">
|
||||
<td>TOTAL / RATA-RATA</td>
|
||||
<td><?php echo e($totAll); ?></td>
|
||||
<td class="pos"><?php echo e($totP); ?></td>
|
||||
<td class="net"><?php echo e($totNt); ?></td>
|
||||
<td class="neg"><?php echo e($totN); ?></td>
|
||||
<td><span class="badge-green"><?php echo e($totAll > 0 ? number_format(($totP/$totAll)*100,2) : 0); ?>%</span></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="donut-box">
|
||||
<h4>Distribusi Sentimen Tahun <?php echo e($tahun); ?></h4>
|
||||
<div class="donut-wrap" style="height:220px;">
|
||||
<canvas id="donut"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section-title">Trend Sentimen per Bulan (<?php echo e($tahun); ?>)</div>
|
||||
|
||||
<div class="trend-box">
|
||||
<canvas id="trend" height="90"></canvas>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
<?php $__currentLoopData = $wisataPopuler; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $i => $w): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<tr>
|
||||
<td><?php echo e($i+1); ?></td>
|
||||
<td><?php echo e($w->wisata); ?></td>
|
||||
<td><?php echo e($w->jumlah); ?></td>
|
||||
<td><span class="badge-green"><?php echo e($w->persen); ?>%</span></td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="kesimpulan-box">
|
||||
<h4>Kesimpulan Tahunan</h4>
|
||||
<p>
|
||||
Secara umum, sentimen positif mendominasi tahun <b><?php echo e($tahun); ?></b>
|
||||
dengan rata-rata <b><?php echo e($persenPositif); ?>%</b>.
|
||||
<?php if(isset($wisataPopuler[0])): ?>
|
||||
<?php echo e($wisataPopuler[0]->wisata); ?> menjadi destinasi paling populer
|
||||
dengan sentimen positif tertinggi.
|
||||
<?php endif; ?>
|
||||
Diperlukan peningkatan pada aspek fasilitas, kebersihan, dan pelayanan
|
||||
untuk menekan sentimen negatif.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="footer">
|
||||
<div>SENTARA - Sistem Analisis Sentimen Wisata Jember</div>
|
||||
<div>Dicetak oleh: <?php echo e(auth()->user()->name); ?> (<?php echo e(ucfirst(auth()->user()->role ?? 'Admin')); ?>)</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── DONUT ──
|
||||
const donutCtx = document.getElementById('donut');
|
||||
const totalUlasan = <?php echo e($totalUlasan > 0 ? $totalUlasan : 1); ?>;
|
||||
const donutData = [<?php echo e($positif); ?>, <?php echo e($netral); ?>, <?php echo e($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: [
|
||||
<?php $__currentLoopData = $rekapBulanan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $r): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
'<?php echo e($r->bulan); ?>',
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Positif',
|
||||
data: [<?php $__currentLoopData = $rekapBulanan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $r): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?><?php echo e($r->positif); ?>,<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>],
|
||||
borderColor: '#22c55e',
|
||||
backgroundColor: 'rgba(34,197,94,0.08)',
|
||||
tension: 0.4, fill: true, pointRadius: 4, pointBackgroundColor: '#22c55e'
|
||||
},
|
||||
{
|
||||
label: 'Netral',
|
||||
data: [<?php $__currentLoopData = $rekapBulanan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $r): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?><?php echo e($r->netral); ?>,<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>],
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: 'rgba(245,158,11,0.05)',
|
||||
tension: 0.4, fill: false, pointRadius: 4, pointBackgroundColor: '#f59e0b',
|
||||
borderDash: [4,3]
|
||||
},
|
||||
{
|
||||
label: 'Negatif',
|
||||
data: [<?php $__currentLoopData = $rekapBulanan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $r): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?><?php echo e($r->negatif); ?>,<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>],
|
||||
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-<?php echo e($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><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/laporan/tahunan.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
<?php
|
||||
$destinasiList ??= collect([]);
|
||||
$totalDestinasiAnalisis ??= 0;
|
||||
$totalDestinasiBerkeluhan ??= 0;
|
||||
$totalNegatif ??= 0;
|
||||
$totalUlasan ??= 0;
|
||||
$persenNegatif ??= 0;
|
||||
$tingkatKepuasan ??= 0;
|
||||
$labelKepuasan ??= 'Kurang';
|
||||
$isuDominan ??= '-';
|
||||
$isuDominanPersen ??= 0;
|
||||
$isuUtama ??= [];
|
||||
$kataDominan ??= [];
|
||||
$saranPerbaikan ??= [];
|
||||
$prioritas ??= [];
|
||||
?>
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">Rekomendasi Layanan</h2>
|
||||
<p class="text-sm text-gray-500">
|
||||
Rekomendasi dibuat dari ulasan negatif agar saran perbaikan fokus pada keluhan pengunjung.
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<?php echo e($totalDestinasiAnalisis); ?> destinasi dianalisis, <?php echo e($totalDestinasiBerkeluhan); ?> destinasi memiliki ulasan negatif.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<form method="GET" action="<?php echo e(route('dashboard')); ?>" class="flex gap-3">
|
||||
<input type="hidden" name="tab" value="rekomendasi">
|
||||
<input type="hidden" name="periode_id" value="<?php echo e($periodeAktif->id ?? request('periode_id')); ?>">
|
||||
<select name="destinasi" onchange="this.form.submit()"
|
||||
class="border rounded-lg pl-3 pr-9 py-2 text-sm min-w-[220px] focus:outline-none focus:ring-2 focus:ring-blue-400">
|
||||
<option value="">Semua Destinasi</option>
|
||||
<?php $__currentLoopData = $destinasiList; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $destinasi): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($destinasi); ?>" <?php echo e(request('destinasi') == $destinasi ? 'selected' : ''); ?>>
|
||||
<?php echo e($destinasi); ?>
|
||||
|
||||
</option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-5">
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
|
||||
<div class="bg-blue-100 text-blue-600 p-3 rounded-full text-xl">📍</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Destinasi Dianalisis</p>
|
||||
<h3 class="text-xl font-bold"><?php echo e($totalDestinasiAnalisis); ?></h3>
|
||||
<p class="text-xs text-gray-500"><?php echo e($totalDestinasiBerkeluhan); ?> punya keluhan</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
|
||||
<div class="bg-red-100 text-red-500 p-3 rounded-full text-xl">😟</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Total Ulasan Negatif</p>
|
||||
<h3 class="text-xl font-bold"><?php echo e($totalNegatif); ?></h3>
|
||||
<p class="text-xs text-red-500"><?php echo e($persenNegatif); ?>% dari total</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
|
||||
<div class="bg-green-100 text-green-600 p-3 rounded-full text-xl">😊</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Tingkat Kepuasan</p>
|
||||
<h3 class="text-xl font-bold"><?php echo e($tingkatKepuasan); ?>%</h3>
|
||||
<span class="text-xs px-2 py-1 rounded
|
||||
<?php echo e($labelKepuasan === 'Baik' ? 'bg-green-100 text-green-700' :
|
||||
($labelKepuasan === 'Sedang' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700')); ?>">
|
||||
<?php echo e($labelKepuasan); ?>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-4">
|
||||
<div class="bg-purple-100 text-purple-600 p-3 rounded-full text-xl">❗</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Isu Dominan</p>
|
||||
<h3 class="text-xl font-bold"><?php echo e($isuDominan); ?></h3>
|
||||
<p class="text-xs text-gray-500"><?php echo e($isuDominanPersen); ?>% dari total isu</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Isu Utama (Top 5)</h3>
|
||||
|
||||
<?php $__empty_1 = true; $__currentLoopData = $isuUtama; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $i => $isu): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
<?php
|
||||
$warnaClass = match($isu['color']) {
|
||||
'red' => 'bg-red-500',
|
||||
'orange' => 'bg-orange-500',
|
||||
'yellow' => 'bg-yellow-400',
|
||||
'green' => 'bg-green-500',
|
||||
default => 'bg-blue-500',
|
||||
};
|
||||
?>
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span><?php echo e($i + 1); ?>. <?php echo e($isu['nama']); ?></span>
|
||||
<span class="font-semibold"><?php echo e($isu['persen']); ?>%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 h-2 rounded">
|
||||
<div class="h-2 rounded <?php echo e($warnaClass); ?>" style="width: <?php echo e($isu['persen']); ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
<p class="text-sm text-gray-400 text-center py-4">Belum ada data isu.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Kata Kunci Dominan</h3>
|
||||
|
||||
<?php $maxFreq = !empty($kataDominan) ? max($kataDominan) : 1; ?>
|
||||
|
||||
<?php $__empty_1 = true; $__currentLoopData = array_slice($kataDominan, 0, 5, true); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $kata => $jumlah): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span><?php echo e($kata); ?></span>
|
||||
<span class="font-semibold text-blue-600"><?php echo e($jumlah); ?>x</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 h-2 rounded mt-1">
|
||||
<div class="bg-blue-500 h-2 rounded" style="width: <?php echo e(round(($jumlah / $maxFreq) * 100)); ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
<p class="text-sm text-gray-400 text-center py-4">Belum ada kata kunci.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Saran Perbaikan</h3>
|
||||
|
||||
<div class="space-y-4 text-sm">
|
||||
<?php $__empty_1 = true; $__currentLoopData = $saranPerbaikan; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $saran): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
<div class="flex gap-3">
|
||||
<div class="text-xl"><?php echo e($saran['icon']); ?></div>
|
||||
<div>
|
||||
<p class="font-semibold"><?php echo e($saran['nama']); ?></p>
|
||||
<p class="text-gray-600"><?php echo e($saran['tip']); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
<p class="text-sm text-gray-400 text-center py-4">Belum ada saran.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<?php $__empty_1 = true; $__currentLoopData = $prioritas; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $p): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
<?php
|
||||
$border = match($p['color']) {
|
||||
'red' => 'border-red-300',
|
||||
'orange' => 'border-orange-300',
|
||||
'yellow' => 'border-yellow-300',
|
||||
'green' => 'border-green-300',
|
||||
default => 'border-blue-300',
|
||||
};
|
||||
$title = match($p['color']) {
|
||||
'red' => 'text-red-600',
|
||||
'orange' => 'text-orange-500',
|
||||
'yellow' => 'text-yellow-600',
|
||||
'green' => 'text-green-600',
|
||||
default => 'text-blue-600',
|
||||
};
|
||||
$dampak = match($p['color']) {
|
||||
'red' => 'bg-red-100 text-red-600',
|
||||
'orange' => 'bg-orange-100 text-orange-600',
|
||||
'yellow' => 'bg-yellow-100 text-yellow-600',
|
||||
'green' => 'bg-green-100 text-green-700',
|
||||
default => 'bg-blue-100 text-blue-600',
|
||||
};
|
||||
?>
|
||||
<div class="border <?php echo e($border); ?> rounded-xl p-5 bg-white">
|
||||
<h4 class="font-semibold <?php echo e($title); ?> mb-2">Prioritas <?php echo e($p['rank']); ?></h4>
|
||||
<h3 class="text-lg font-bold mb-3"><?php echo e($p['nama']); ?></h3>
|
||||
|
||||
<ul class="text-sm space-y-2 mb-4">
|
||||
<?php $__currentLoopData = $p['actions']; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $action): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<li class="flex items-center gap-2">
|
||||
<span class="text-blue-500">✔</span> <?php echo e($action); ?>
|
||||
|
||||
</li>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</ul>
|
||||
|
||||
<div class="text-xs <?php echo e($dampak); ?> p-2 rounded">
|
||||
Dampak: <?php echo e($p['dampak']); ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
<div class="col-span-3 text-center text-gray-400 text-sm py-6">
|
||||
Belum ada data rekomendasi. Pastikan analisis sentimen sudah dijalankan.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/rekomendasi/index.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
<?php $__env->startSection('content'); ?>
|
||||
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">
|
||||
Riwayat Analisis
|
||||
</h2>
|
||||
|
||||
<p class="text-sm text-gray-500">
|
||||
Periode tersimpan otomatis setiap kali Ambil Data dijalankan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<form
|
||||
action="<?php echo e(route('laporan.tahunan')); ?>"
|
||||
method="GET"
|
||||
class="flex items-center gap-3">
|
||||
|
||||
<div class="relative">
|
||||
|
||||
<select
|
||||
name="tahun"
|
||||
required
|
||||
class="appearance-none
|
||||
border-2 border-blue-500
|
||||
rounded-2xl
|
||||
pl-4 pr-9
|
||||
py-2.5
|
||||
text-sm font-medium
|
||||
bg-white
|
||||
shadow-sm
|
||||
cursor-pointer
|
||||
focus:outline-none
|
||||
focus:ring-2
|
||||
focus:ring-blue-300">
|
||||
|
||||
<?php for($tahun = date('Y'); $tahun >= 2024; $tahun--): ?>
|
||||
<option value="<?php echo e($tahun); ?>"><?php echo e($tahun); ?></option>
|
||||
<?php endfor; ?>
|
||||
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-blue-600
|
||||
hover:bg-blue-700
|
||||
text-white
|
||||
px-5 py-2.5
|
||||
rounded-2xl
|
||||
text-sm font-medium
|
||||
shadow">
|
||||
Cetak Tahunan
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if(session('success')): ?>
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded">
|
||||
<?php echo e(session('success')); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(session('error')): ?>
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded whitespace-pre-line">
|
||||
<?php echo e(session('error')); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
|
||||
<h3 class="font-semibold text-gray-800 mb-1">
|
||||
Import Data Lama dari CSV
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Gunakan untuk memasukkan ulasan historis. Kolom wajib:
|
||||
wisata, rating, ulasan, tanggal.
|
||||
Kolom reviewer boleh ada.
|
||||
</p>
|
||||
|
||||
<form
|
||||
action="<?php echo e(route('riwayat.import')); ?>"
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
class="grid grid-cols-1 md:grid-cols-[180px_1fr_auto] gap-3 md:items-center">
|
||||
|
||||
<?php echo csrf_field(); ?>
|
||||
|
||||
<input
|
||||
type="month"
|
||||
name="periode_bulan"
|
||||
value="<?php echo e(now()->format('Y-m')); ?>"
|
||||
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-[150px]"
|
||||
required>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
accept=".csv,.txt"
|
||||
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-0"
|
||||
required>
|
||||
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-lg text-sm shadow">
|
||||
|
||||
Import CSV
|
||||
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<?php if($errors->any()): ?>
|
||||
<div class="mt-3 text-sm text-red-600">
|
||||
<?php echo e($errors->first()); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
|
||||
<table class="w-full text-sm">
|
||||
|
||||
<thead class="bg-gray-100 text-left">
|
||||
|
||||
<tr>
|
||||
|
||||
<th class="px-4 py-3">
|
||||
Periode
|
||||
</th>
|
||||
|
||||
<th class="px-4 py-3 text-center">
|
||||
Total Ulasan
|
||||
</th>
|
||||
|
||||
<th class="px-4 py-3 text-center">
|
||||
Hasil Analisis
|
||||
</th>
|
||||
|
||||
<th class="px-4 py-3 text-center">
|
||||
Wisata
|
||||
</th>
|
||||
|
||||
<th class="px-4 py-3">
|
||||
Terakhir Analisis
|
||||
</th>
|
||||
|
||||
<th class="px-4 py-3 text-center">
|
||||
Aksi
|
||||
</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y">
|
||||
|
||||
<?php $__empty_1 = true; $__currentLoopData = $riwayat; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
|
||||
<tr class="hover:bg-gray-50">
|
||||
|
||||
<td class="px-4 py-3 font-semibold text-gray-800">
|
||||
<?php echo e($item->nama); ?>
|
||||
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-center">
|
||||
<?php echo e($item->total_ulasan); ?>
|
||||
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-center">
|
||||
<?php echo e($item->total_hasil); ?>
|
||||
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-center">
|
||||
<?php echo e($item->total_wisata); ?>
|
||||
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3 text-gray-500">
|
||||
|
||||
<?php echo e($item->terakhir_analisis
|
||||
? \Carbon\Carbon::parse(
|
||||
$item->terakhir_analisis
|
||||
)->format('d M Y H:i')
|
||||
: '-'); ?>
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
<td class="px-4 py-3">
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
|
||||
<a
|
||||
href="<?php echo e(route('dashboard',
|
||||
['periode_id'=>$item->id,
|
||||
'tab'=>'analisis'])); ?>"
|
||||
|
||||
class="px-3 py-1 rounded-lg
|
||||
bg-green-50 text-green-600
|
||||
hover:bg-green-100">
|
||||
|
||||
Analisis
|
||||
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="<?php echo e(route('ulasan.index',
|
||||
['periode_id'=>$item->id])); ?>"
|
||||
|
||||
class="px-3 py-1 rounded-lg
|
||||
bg-gray-100 text-gray-700
|
||||
hover:bg-gray-200">
|
||||
|
||||
Ulasan
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
<a
|
||||
href="<?php echo e(route('laporan.bulanan', $item->id)); ?>"
|
||||
class="px-3 py-1 rounded-lg
|
||||
bg-blue-50 text-blue-600
|
||||
hover:bg-blue-100">
|
||||
Cetak Laporan
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
colspan="6"
|
||||
class="px-4 py-8 text-center text-gray-400">
|
||||
|
||||
Belum ada riwayat analisis.
|
||||
Jalankan Ambil Data terlebih dahulu.
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<?php if($riwayat->hasPages()): ?>
|
||||
<?php
|
||||
$current = $riwayat->currentPage();
|
||||
$last = $riwayat->lastPage();
|
||||
$query = http_build_query(request()->except('page'));
|
||||
$window = collect(range(max(1, $current - 2), min($last, $current + 2)));
|
||||
?>
|
||||
|
||||
<div class="px-6 py-4 border-t border-gray-100 flex items-center justify-between text-sm text-gray-600">
|
||||
|
||||
|
||||
<p>Menampilkan <?php echo e($riwayat->firstItem()); ?> – <?php echo e($riwayat->lastItem()); ?> dari <?php echo e($riwayat->total()); ?> data</p>
|
||||
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
|
||||
|
||||
<?php if($riwayat->onFirstPage()): ?>
|
||||
<span class="px-3 py-1.5 rounded-lg border bg-gray-100 text-gray-400 cursor-not-allowed">«</span>
|
||||
<?php else: ?>
|
||||
<a href="<?php echo e($riwayat->previousPageUrl()); ?>&<?php echo e($query); ?>" class="px-3 py-1.5 rounded-lg border hover:bg-blue-50 text-blue-600 transition">«</a>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if(!$window->contains(1)): ?>
|
||||
<a href="<?php echo e($riwayat->url(1)); ?>&<?php echo e($query); ?>" class="px-3 py-1.5 rounded-lg border hover:bg-blue-50 text-blue-600 transition">1</a>
|
||||
<?php if($window->min() > 2): ?>
|
||||
<span class="px-2 py-1.5 text-gray-400">...</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php $__currentLoopData = $window; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $page): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php if($page == $current): ?>
|
||||
<span class="px-3 py-1.5 rounded-lg border bg-blue-600 text-white font-semibold"><?php echo e($page); ?></span>
|
||||
<?php else: ?>
|
||||
<a href="<?php echo e($riwayat->url($page)); ?>&<?php echo e($query); ?>" class="px-3 py-1.5 rounded-lg border hover:bg-blue-50 text-blue-600 transition"><?php echo e($page); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
|
||||
|
||||
<?php if(!$window->contains($last)): ?>
|
||||
<?php if($window->max() < $last - 1): ?>
|
||||
<span class="px-2 py-1.5 text-gray-400">...</span>
|
||||
<?php endif; ?>
|
||||
<a href="<?php echo e($riwayat->url($last)); ?>&<?php echo e($query); ?>" class="px-3 py-1.5 rounded-lg border hover:bg-blue-50 text-blue-600 transition"><?php echo e($last); ?></a>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if($riwayat->hasMorePages()): ?>
|
||||
<a href="<?php echo e($riwayat->nextPageUrl()); ?>&<?php echo e($query); ?>" class="px-3 py-1.5 rounded-lg border hover:bg-blue-50 text-blue-600 transition">»</a>
|
||||
<?php else: ?>
|
||||
<span class="px-3 py-1.5 rounded-lg border bg-gray-100 text-gray-400 cursor-not-allowed">»</span>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/ulasan/riwayat.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -1,354 +0,0 @@
|
|||
<?php $__env->startSection('content'); ?>
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
|
||||
<?php if(session('success')): ?>
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-6 rounded">
|
||||
<?php echo e(session('success')); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if(session('error')): ?>
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded whitespace-pre-line">
|
||||
<?php echo e(session('error')); ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if($noDataPesan ?? null): ?>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 text-blue-700 p-4 mb-6 rounded flex items-center gap-2">
|
||||
<span>📅</span> <span><?php echo e($noDataPesan); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if($periodeAktif && !$hasAnalisis): ?>
|
||||
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-6 rounded">
|
||||
Data ulasan sudah tersedia, tetapi belum dilakukan analisis sentimen.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-gray-600 font-medium">
|
||||
Periode aktif: <?php echo e($periodeAktif->nama ?? 'Belum ada data'); ?>
|
||||
|
||||
</span>
|
||||
<a href="<?php echo e(route('riwayat.index')); ?>" class="text-sm text-blue-600 hover:underline">
|
||||
Lihat riwayat
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<?php if($periodeAktif): ?>
|
||||
<span class="text-xs text-gray-400">
|
||||
Terakhir update: <?php echo e($lastUpdate ? \Carbon\Carbon::parse($lastUpdate)->format('d M Y H:i') : '-'); ?>
|
||||
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<form method="GET"
|
||||
action="<?php echo e(route('dashboard')); ?>"
|
||||
class="flex flex-col md:flex-row gap-3 md:items-center">
|
||||
|
||||
|
||||
|
||||
<input type="month"
|
||||
name="periode_bulan"
|
||||
value="<?php echo e(request('periode_bulan', now()->format('Y-m'))); ?>"
|
||||
max="<?php echo e(now()->format('Y-m')); ?>"
|
||||
onchange="this.form.submit()"
|
||||
class="border rounded-lg px-3 py-2 text-sm bg-white shadow-sm min-w-[150px]">
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div x-data="{ tab: new URLSearchParams(window.location.search).get('tab') || 'dashboard' }"
|
||||
x-init="$nextTick(() => { if (tab === 'dashboard') renderAllCharts() })"
|
||||
class="mb-6">
|
||||
|
||||
<!-- TAB -->
|
||||
<div class="bg-blue-700 rounded-xl p-1 flex space-x-2 shadow mb-6">
|
||||
<button @click="tab='dashboard'; $nextTick(() => renderAllCharts())"
|
||||
:class="tab==='dashboard' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Dashboard Sentara
|
||||
</button>
|
||||
<button @click="tab='analisis'"
|
||||
:class="tab==='analisis' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Hasil Analisis Sentimen
|
||||
</button>
|
||||
<button @click="tab='rekomendasi'"
|
||||
:class="tab==='rekomendasi' ? 'bg-white text-blue-700 shadow' : 'text-white hover:bg-blue-600'"
|
||||
class="flex-1 py-3 rounded-lg text-sm font-medium transition">
|
||||
Rekomendasi Layanan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ================= DASHBOARD ================= -->
|
||||
<div x-show="tab === 'dashboard'">
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Total Ulasan</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-gray-900"><?php echo e($stats['total']); ?></h3>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Positif</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-green-600"><?php echo e($stats['positif_persen']); ?>%</h3>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Negatif</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-red-600"><?php echo e($stats['negatif_persen']); ?>%</h3>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-5">
|
||||
<p class="text-gray-500 text-sm">Sentimen Netral</p>
|
||||
<h3 class="text-2xl font-bold mt-2 text-yellow-500"><?php echo e($stats['netral_persen']); ?>%</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Distribusi Sentimen</h3>
|
||||
<div style="height:300px;">
|
||||
<canvas id="pieChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Sentimen per Destinasi</h3>
|
||||
<div class="h-[300px]">
|
||||
<canvas id="destinationChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">Total Data per Destinasi</h3>
|
||||
<div class="space-y-4 text-sm text-gray-600">
|
||||
<?php $__currentLoopData = $totalPerWisata; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<div class="flex justify-between border-b pb-3">
|
||||
<span><?php echo e($item->wisata); ?></span>
|
||||
<span class="font-semibold text-gray-800"><?php echo e($item->total); ?> ulasan</span>
|
||||
</div>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">Word Cloud</h3>
|
||||
<div id="wordCloud" class="w-full h-[240px] overflow-hidden flex flex-wrap content-center justify-center gap-3 text-center">
|
||||
<p class="text-sm text-gray-400">Belum ada kata dominan.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ================= ANALISIS ================= -->
|
||||
<div x-show="tab === 'analisis'" id="analisis-content">
|
||||
<?php echo $__env->make('analisis.index', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
|
||||
</div>
|
||||
|
||||
<!-- ================= REKOMENDASI ================= -->
|
||||
<div x-show="tab === 'rekomendasi'" id="rekomendasi-content">
|
||||
<?php echo $__env->make('rekomendasi.index', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
const pieData = <?php echo json_encode($chartSentimen, 15, 512) ?>;
|
||||
const destinationData = <?php echo json_encode($chartDestinasi, 15, 512) ?>;
|
||||
const text = <?php echo json_encode($allText, 15, 512) ?>;
|
||||
|
||||
function renderAllCharts() {
|
||||
// PIE CHART
|
||||
const pieCanvas = document.getElementById('pieChart');
|
||||
if (pieCanvas) {
|
||||
if (window.pieChartInstance) {
|
||||
window.pieChartInstance.destroy();
|
||||
window.pieChartInstance = null;
|
||||
}
|
||||
|
||||
// Reset canvas dengan clone agar benar-benar bersih
|
||||
const newPie = pieCanvas.cloneNode(false);
|
||||
pieCanvas.parentNode.replaceChild(newPie, pieCanvas);
|
||||
|
||||
window.pieChartInstance = new Chart(newPie, { // <-- newPie, bukan pieCanvas
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: pieData.map(i => i.sentimen),
|
||||
datasets: [{
|
||||
data: pieData.map(i => Number(i.total)),
|
||||
backgroundColor: ['#3B82F6', '#EF4444', '#F59E0B']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// BAR CHART
|
||||
const destinationCanvas = document.getElementById('destinationChart');
|
||||
if (destinationCanvas) {
|
||||
if (window.destinationChartInstance) {
|
||||
window.destinationChartInstance.destroy();
|
||||
window.destinationChartInstance = null;
|
||||
}
|
||||
window.destinationChartInstance = new Chart(destinationCanvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: destinationData.map(item => item.wisata),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Positif',
|
||||
data: destinationData.map(item => Number(item.positif || 0)),
|
||||
backgroundColor: 'rgba(96, 165, 250, 0.88)',
|
||||
borderColor: '#3B82F6',
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
},
|
||||
{
|
||||
label: 'Negatif',
|
||||
data: destinationData.map(item => Number(item.negatif || 0)),
|
||||
backgroundColor: 'rgba(248, 113, 113, 0.9)',
|
||||
borderColor: '#EF4444',
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
},
|
||||
{
|
||||
label: 'Netral',
|
||||
data: destinationData.map(item => Number(item.netral || 0)),
|
||||
backgroundColor: 'rgba(251, 191, 36, 0.72)',
|
||||
borderColor: '#F59E0B',
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
align: 'center',
|
||||
labels: { boxWidth: 38, boxHeight: 10, color: '#4B5563', font: { size: 12 } },
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: context => `${context.dataset.label}: ${context.parsed.y} ulasan`,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(156, 163, 175, 0.22)' },
|
||||
ticks: { color: '#4B5563', maxRotation: 14, minRotation: 14, font: { size: 12 } },
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { color: '#4B5563', precision: 0 },
|
||||
grid: { color: 'rgba(156, 163, 175, 0.28)' },
|
||||
},
|
||||
},
|
||||
categoryPercentage: 0.72,
|
||||
barPercentage: 0.82,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// WORD CLOUD
|
||||
const wordCloud = document.getElementById('wordCloud');
|
||||
const stopwords = new Set([
|
||||
'yang','dan','dari','untuk','dengan','ini','itu','tidak','ada','juga',
|
||||
'sangat','lebih','sudah','bisa','tapi','karena','pada','atau','saya',
|
||||
'kami','mereka','nya','jadi','kalau','dalam','akan','saat','tempat'
|
||||
]);
|
||||
|
||||
if (wordCloud && text.trim().length > 0) {
|
||||
const wordCount = {};
|
||||
text.toLowerCase().split(/\s+/).forEach(word => {
|
||||
const cleanWord = word.replace(/[^a-z]/g, '');
|
||||
if (cleanWord.length > 3 && !stopwords.has(cleanWord)) {
|
||||
wordCount[cleanWord] = (wordCount[cleanWord] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const words = Object.entries(wordCount)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 28);
|
||||
|
||||
if (words.length > 0) {
|
||||
const max = words[0][1] || 1;
|
||||
const colors = ['text-blue-700', 'text-green-600', 'text-red-500', 'text-yellow-600', 'text-indigo-600'];
|
||||
wordCloud.innerHTML = words.map(([word, count], index) => {
|
||||
const size = 13 + Math.round((count / max) * 20);
|
||||
const weight = count === max ? 800 : 600;
|
||||
return `<span class="${colors[index % colors.length]} inline-block" style="font-size:${size}px;font-weight:${weight}" title="${count}x">${word}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const container = document.getElementById('analisis-content');
|
||||
|
||||
// FILTER AJAX
|
||||
document.addEventListener('submit', function(e){
|
||||
if(e.target.id === 'filter-form'){
|
||||
e.preventDefault();
|
||||
const params = new URLSearchParams(new FormData(e.target)).toString();
|
||||
fetch('/dashboard?' + params)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PAGINATION AJAX
|
||||
document.addEventListener('click', function(e){
|
||||
const link = e.target.closest('a');
|
||||
if(link && link.href.includes('page=')){
|
||||
e.preventDefault();
|
||||
fetch(link.href)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
container.innerHTML = doc.getElementById('analisis-content').innerHTML;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/dashboard.blade.php ENDPATH**/ ?>
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
<?php if($standalone ?? false): ?>
|
||||
|
||||
<?php $__env->startSection('content'); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$totalHasilAnalisis ??= $hasil->total() ?? 0;
|
||||
$jumlahKelasAnalisis ??= 0;
|
||||
$evaluasiTersedia = $evaluasi && $jumlahKelasAnalisis >= 2;
|
||||
?>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">Hasil Analisis Sentimen</h2>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Evaluasi memakai pseudo-label dari rating/rule otomatis, bukan anotasi manual.
|
||||
</p>
|
||||
<?php if($totalHasilAnalisis > 0 && !$evaluasiTersedia): ?>
|
||||
<p class="text-sm text-yellow-700 bg-yellow-50 border border-yellow-200 rounded-lg px-3 py-2 mt-3">
|
||||
Semua hasil hanya memiliki <?php echo e($jumlahKelasAnalisis); ?> kelas sentimen. Precision, recall, F1, dan akurasi tidak dihitung karena evaluasi klasifikasi membutuhkan minimal 2 kelas. Detail hasil analisis tetap ditampilkan di bawah.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">Precision</p>
|
||||
<h3 class="text-2xl font-bold text-blue-600">
|
||||
<?php echo e($evaluasiTersedia ? number_format($evaluasi->precision ?? 0, 2) : '-'); ?>
|
||||
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">Recall</p>
|
||||
<h3 class="text-2xl font-bold text-green-600">
|
||||
<?php echo e($evaluasiTersedia ? number_format($evaluasi->recall ?? 0, 2) : '-'); ?>
|
||||
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">F1 Score</p>
|
||||
<h3 class="text-2xl font-bold text-purple-600">
|
||||
<?php echo e($evaluasiTersedia ? number_format($evaluasi->f1_score ?? 0, 2) : '-'); ?>
|
||||
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow">
|
||||
<p class="text-gray-500 text-sm">Akurasi</p>
|
||||
<h3 class="text-2xl font-bold text-indigo-600">
|
||||
<?php echo e($evaluasiTersedia ? number_format($evaluasi->accuracy ?? 0, 2) : '-'); ?>
|
||||
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="flex items-end gap-3 mb-4">
|
||||
|
||||
<form id="filter-form"
|
||||
method="GET"
|
||||
action="<?php echo e(route('dashboard')); ?>"
|
||||
class="flex items-end gap-2">
|
||||
|
||||
<input type="hidden" name="tab" value="analisis">
|
||||
<input type="hidden" name="periode_id"
|
||||
value="<?php echo e($periodeAktif->id ?? ''); ?>">
|
||||
|
||||
<select
|
||||
name="wisata"
|
||||
class="border rounded px-3 py-2 h-[46px]">
|
||||
|
||||
<option value="">Semua Destinasi</option>
|
||||
|
||||
<?php $__currentLoopData = $wisataList; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<option value="<?php echo e($item); ?>"
|
||||
<?php echo e(request('wisata') == $item ? 'selected' : ''); ?>>
|
||||
<?php echo e($item); ?>
|
||||
|
||||
</option>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-blue-600 text-white px-4 rounded h-[46px]">
|
||||
Filter
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<a href="<?php echo e(route('riwayat.index')); ?>"
|
||||
class="inline-flex items-center justify-center bg-blue-600 hover:bg-blue-700 text-white px-5 rounded-lg shadow transition h-[46px]">
|
||||
|
||||
<i class="fas fa-history mr-2"></i>
|
||||
Lihat Riwayat
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-white rounded-xl shadow p-4 h-fit">
|
||||
|
||||
<h3 class="font-semibold mb-4">Detail Hasil Analisis Sentimen</h3>
|
||||
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b text-left">
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Wisata</th>
|
||||
<th>Ulasan Asli</th>
|
||||
<th>Ulasan Bersih</th>
|
||||
<th>Sentimen</th>
|
||||
<th>Probabilitas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<?php $__empty_1 = true; $__currentLoopData = $hasil; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); $__empty_1 = false; ?>
|
||||
<tr class="border-b">
|
||||
<td><?php echo e(($hasil->currentPage() - 1) * $hasil->perPage() + $loop->iteration); ?></td>
|
||||
<td><?php echo e($item->wisata); ?></td>
|
||||
<td class="text-gray-500"><?php echo e(Str::limit($item->ulasan_asli ?? '-', 80)); ?></td>
|
||||
<td><?php echo e(Str::limit($item->hasil_preprocessing ?? '-', 80)); ?></td>
|
||||
<td>
|
||||
<span class="px-2 py-1 rounded text-xs
|
||||
<?php echo e(strtolower($item->sentimen) == 'positif' ? 'bg-green-100 text-green-600' : ''); ?>
|
||||
|
||||
<?php echo e(strtolower($item->sentimen) == 'negatif' ? 'bg-red-100 text-red-600' : ''); ?>
|
||||
|
||||
<?php echo e(strtolower($item->sentimen) == 'netral' ? 'bg-yellow-100 text-yellow-600' : ''); ?>
|
||||
|
||||
">
|
||||
<?php echo e(ucfirst($item->sentimen)); ?>
|
||||
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo e(number_format($item->probabilitas ?? 0, 2)); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); if ($__empty_1): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4 text-gray-500">Tidak ada data</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<?php if($hasil->hasPages()): ?>
|
||||
<?php
|
||||
$current = $hasil->currentPage();
|
||||
$last = $hasil->lastPage();
|
||||
$query = http_build_query(request()->except('page'));
|
||||
// Window 2 halaman kiri & kanan dari current
|
||||
$window = collect(range(max(1, $current - 2), min($last, $current + 2)));
|
||||
?>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-gray-600">
|
||||
|
||||
|
||||
<p>Menampilkan <?php echo e($hasil->firstItem()); ?> - <?php echo e($hasil->lastItem()); ?> dari <?php echo e($hasil->total()); ?> data</p>
|
||||
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
|
||||
|
||||
<?php if($hasil->onFirstPage()): ?>
|
||||
<span class="px-3 py-1 rounded border bg-gray-100 text-gray-400 cursor-not-allowed">«</span>
|
||||
<?php else: ?>
|
||||
<a href="<?php echo e($hasil->previousPageUrl()); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">«</a>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if(!$window->contains(1)): ?>
|
||||
<a href="<?php echo e($hasil->url(1)); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">1</a>
|
||||
<?php if($window->min() > 2): ?>
|
||||
<span class="px-2 py-1 text-gray-400">...</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php $__currentLoopData = $window; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $page): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||
<?php if($page == $current): ?>
|
||||
<span class="px-3 py-1 rounded border bg-blue-600 text-white font-semibold"><?php echo e($page); ?></span>
|
||||
<?php else: ?>
|
||||
<a href="<?php echo e($hasil->url($page)); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600"><?php echo e($page); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||
|
||||
|
||||
<?php if(!$window->contains($last)): ?>
|
||||
<?php if($window->max() < $last - 1): ?>
|
||||
<span class="px-2 py-1 text-gray-400">...</span>
|
||||
<?php endif; ?>
|
||||
<a href="<?php echo e($hasil->url($last)); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600"><?php echo e($last); ?></a>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php if($hasil->hasMorePages()): ?>
|
||||
<a href="<?php echo e($hasil->nextPageUrl()); ?>&<?php echo e($query); ?>" class="px-3 py-1 rounded border hover:bg-blue-50 text-blue-600">»</a>
|
||||
<?php else: ?>
|
||||
<span class="px-3 py-1 rounded border bg-gray-100 text-gray-400 cursor-not-allowed">»</span>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if($standalone ?? false): ?>
|
||||
<?php $__env->stopSection(); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php echo $__env->make('layouts.sentara', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH C:\laragon\www\SistemAnalisisSentimen\resources\views/analisis/index.blade.php ENDPATH**/ ?>
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue