MIF_E31222544/backup/DashboardView.vue

436 lines
15 KiB
Vue

<template>
<div class="fixed inset-0 w-full h-full flex bg-dashboard-bg bg-cover bg-center">
<!-- Sidebar -->
<div class="absolute left-5 top-5 h-5/6 bg-gray-700 bg-opacity-90 border-r-4 border-gray-500 pixel-border shadow-lg w-16 flex flex-col items-center p-4">
<nav class="flex flex-col space-y-6 text-center mt-4">
<a href="/dashboard" class="text-white hover:text-yellow-400 transition">
<LayoutDashboard class="w-8 h-8" />
</a>
<a href="/scraper" class="text-white hover:text-blue-400 transition">
<Bot class="w-8 h-8" />
</a>
</nav>
</div>
<!-- Main Content -->
<div class="flex-1 ml-20 overflow-y-auto p-4">
<Modal v-if="isModalOpen" @close="isModalOpen = false">
<template #header>
<h3 class="text-xl font-bold text-white">Edit Data {{ modalTitle }}</h3>
</template>
<template #body>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-white table-auto">
<thead>
<tr>
<th v-for="(header, index) in tableHeaders" :key="index" class="px-4 py-2 border-b border-gray-500">{{ header }}</th>
<th class="px-4 py-2 border-b border-gray-500">Aksi</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in editableData" :key="rowIndex">
<td v-for="(header, colIndex) in tableHeaders" :key="colIndex" class="px-4 py-2 border-b border-gray-600">
<input v-model="editableData[rowIndex][header]" class="bg-gray-700 text-white px-2 py-1 w-full rounded" />
</td>
<td class="px-4 py-2 border-b border-gray-600 text-center">
<button @click="removeRow(rowIndex)" class="text-red-500 hover:underline">Hapus</button>
</td>
</tr>
</tbody>
</table>
<button @click="addRow" class="mt-4 bg-green-500 hover:bg-green-400 text-white px-4 py-2 rounded">Tambah Baris</button>
</div>
</template>
<template #footer>
<button @click="saveChanges" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded">Simpan Perubahan</button>
</template>
</Modal>
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
<h2 class="text-white text-xl font-bold mb-6">Dashboard Scraper</h2>
<button @click="refreshData" class="text-white bg-green-600 hover:bg-green-500 px-4 py-2 rounded-2xl mb-10">
Refresh Grafik
</button>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
<!-- Pengabdian -->
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
<h3 class="text-lg font-semibold mb-2">Pengabdian</h3>
<button @click="openModal('pengabdian')" class="text-sm text-yellow-400 hover:underline">Edit</button>
<div
@dragover.prevent
@drop.prevent="handleDrop"
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
>
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
</div>
<div v-if="pengabdian.length > 0" class="mt-4">
<BarChart :data="pengabdianChartData" />
</div>
</div>
<!-- HKI -->
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
<h3 class="text-lg font-semibold mb-2">HKI</h3>
<div
@dragover.prevent
@drop.prevent="handleDropHKI"
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
>
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
</div>
<div v-if="hki.length > 0" class="mt-4">
<BarChart :data="hkiChartData" />
</div>
</div>
<!-- Penelitian -->
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between">
<h3 class="text-lg font-semibold mb-2">Penelitian</h3>
<div
@dragover.prevent
@drop.prevent="handleDropPenelitian"
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
>
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
</div>
<div v-if="penelitian.length > 0" class="mt-4">
<BarChart :data="penelitianChartData" />
</div>
</div>
<!-- Scopus -->
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
<h3 class="text-lg font-semibold mb-2">Scopus</h3>
<div
@dragover.prevent
@drop.prevent="handleDropScopus"
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
>
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
</div>
<div v-if="scopus.length > 0" class="mt-4">
<BarChart :data="scopusChartData" />
</div>
</div>
<div class="bg-gray-800 p-4 rounded-lg text-white w-full h-96 flex flex-col justify-between mt-6">
<h3 class="text-lg font-semibold mb-2">Scholar</h3>
<div
@dragover.prevent
@drop.prevent="handleDropScholar"
class="border-2 border-dashed border-gray-400 p-4 rounded-lg text-center cursor-pointer h-32 flex items-center justify-center hover:border-yellow-500"
>
<span class="text-sm text-gray-300">Tarik file Excel ke sini (drag & drop)</span>
</div>
<div v-if="scholar.length > 0" class="mt-4">
<BarChart :data="scholarChartData" />
</div>
</div>
</div>
</div>
<div class="w-full bg-gray-700 bg-opacity-90 border-4 border-gray-500 pixel-border flex flex-col items-center p-6">
</div>
</div>
</div>
</template>
<script setup>
import Modal from '@/components/Modal.vue'
import axios from 'axios'
import { LayoutDashboard, Bot, Settings, LogOut } from "lucide-vue-next"
import { computed, defineComponent, h } from 'vue'
import { ref } from 'vue'
import * as XLSX from 'xlsx'
import { Bar } from 'vue-chartjs'
import {
Chart as ChartJS,
Title, Tooltip, Legend,
BarElement, CategoryScale, LinearScale
} from 'chart.js'
import { onMounted } from 'vue'
onMounted(() => {
fetchCleanedFile('pengabdian', 'Pengabdian_cleaned.xlsx', pengabdian)
fetchCleanedFile('penelitian', 'Penelitian_cleaned.xlsx', penelitian)
fetchCleanedFile('hki', 'HKI_cleaned.xlsx', hki)
fetchCleanedFile('scholar', 'Scholar_cleaned.xlsx', scholar)
fetchCleanedFile('scopus', 'Scopus_cleaned.xlsx', scopus)
})
function refreshData() {
fetchCleanedFile('pengabdian', 'Pengabdian_cleaned.xlsx', pengabdian)
fetchCleanedFile('penelitian', 'Penelitian_cleaned.xlsx', penelitian)
fetchCleanedFile('hki', 'HKI_cleaned.xlsx', hki)
fetchCleanedFile('scholar', 'Scholar_cleaned.xlsx', scholar)
fetchCleanedFile('scopus', 'Scopus_cleaned.xlsx', scopus)
}
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
const pengabdian = ref([])
const hki = ref([])
const penelitian = ref([])
const scopus = ref([])
const scholar = ref([])
const isModalOpen = ref(false)
const modalTitle = ref('')
const editableData = ref([])
const tableHeaders = ref([])
const currentTargetArray = ref(null)
function openModal(type) {
modalTitle.value = type.charAt(0).toUpperCase() + type.slice(1)
isModalOpen.value = true
let sourceData
if (type === 'pengabdian') {
sourceData = pengabdian.value
currentTargetArray.value = pengabdian
} else if (type === 'hki') {
sourceData = hki.value
currentTargetArray.value = hki
} else if (type === 'penelitian') {
sourceData = penelitian.value
currentTargetArray.value = penelitian
} else if (type === 'scopus') {
sourceData = scopus.value
currentTargetArray.value = scopus
} else if (type === 'scholar') {
sourceData = scholar.value
currentTargetArray.value = scholar
}
editableData.value = JSON.parse(JSON.stringify(sourceData))
tableHeaders.value = Object.keys(sourceData[0] || {})
}
function addRow() {
const newRow = {}
tableHeaders.value.forEach(h => newRow[h] = '')
editableData.value.push(newRow)
}
function removeRow(index) {
editableData.value.splice(index, 1)
}
function saveChanges() {
currentTargetArray.value.value = JSON.parse(JSON.stringify(editableData.value))
isModalOpen.value = false
}
async function fetchCleanedFile(folder, filename, targetArray) {
try {
const response = await axios.get(`http://localhost:8000/cleaned-files/${folder}/${filename}`, {
responseType: 'arraybuffer'
})
const data = new Uint8Array(response.data)
const workbook = XLSX.read(data, { type: 'array' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const json = XLSX.utils.sheet_to_json(sheet)
targetArray.value = json
} catch (error) {
console.error(`Gagal mengambil file cleaned (${folder}/${filename}):`, error)
}
}
function handleDrop(e) {
const file = e.dataTransfer.files[0]
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
const reader = new FileReader()
reader.onload = (evt) => {
const data = new Uint8Array(evt.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const json = XLSX.utils.sheet_to_json(sheet)
pengabdian.value = json
}
reader.readAsArrayBuffer(file)
}
}
function handleDropScholar(e) {
const file = e.dataTransfer.files[0]
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
const reader = new FileReader()
reader.onload = (evt) => {
const data = new Uint8Array(evt.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const json = XLSX.utils.sheet_to_json(sheet)
scholar.value = json
}
reader.readAsArrayBuffer(file)
}
}
const scholarChartData = computed(() => {
const citationsByYear = {}
for (const item of scholar.value) {
const year = item.Tahun || 'Unknown'
const citation = parseInt(item['Total Kutipan']) || 0
citationsByYear[year] = (citationsByYear[year] || 0) + citation
}
return {
labels: Object.keys(citationsByYear),
datasets: [
{
label: 'Total Kutipan Scholar per Tahun',
backgroundColor: '#f472b6', // pink
data: Object.values(citationsByYear),
}
]
}
})
const pengabdianChartData = computed(() => {
const countByYear = {}
for (const item of pengabdian.value) {
const year = item.Tahun || 'Unknown'
countByYear[year] = (countByYear[year] || 0) + 1
}
return {
labels: Object.keys(countByYear),
datasets: [
{
label: 'Jumlah Pengabdian per Tahun',
backgroundColor: '#facc15',
data: Object.values(countByYear),
}
]
}
})
function handleDropHKI(e) {
const file = e.dataTransfer.files[0]
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
const reader = new FileReader()
reader.onload = (evt) => {
const data = new Uint8Array(evt.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const json = XLSX.utils.sheet_to_json(sheet)
hki.value = json
}
reader.readAsArrayBuffer(file)
}
}
const hkiChartData = computed(() => {
const countByYear = {}
for (const item of hki.value) {
const year = item.Tahun || 'Unknown'
countByYear[year] = (countByYear[year] || 0) + 1
}
return {
labels: Object.keys(countByYear),
datasets: [
{
label: 'Jumlah HKI per Tahun',
backgroundColor: '#38bdf8',
data: Object.values(countByYear),
}
]
}
})
function handleDropPenelitian(e) {
const file = e.dataTransfer.files[0]
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
const reader = new FileReader()
reader.onload = (evt) => {
const data = new Uint8Array(evt.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const json = XLSX.utils.sheet_to_json(sheet)
penelitian.value = json
}
reader.readAsArrayBuffer(file)
}
}
const penelitianChartData = computed(() => {
const countByYear = {}
for (const item of penelitian.value) {
const year = item.Tahun || 'Unknown'
countByYear[year] = (countByYear[year] || 0) + 1
}
return {
labels: Object.keys(countByYear),
datasets: [
{
label: 'Jumlah Penelitian per Tahun',
backgroundColor: '#34d399', // hijau
data: Object.values(countByYear),
}
]
}
})
function handleDropScopus(e) {
const file = e.dataTransfer.files[0]
if (file && file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
const reader = new FileReader()
reader.onload = (evt) => {
const data = new Uint8Array(evt.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const json = XLSX.utils.sheet_to_json(sheet)
scopus.value = json
}
reader.readAsArrayBuffer(file)
}
}
const scopusChartData = computed(() => {
const countByQuartile = {}
for (const item of scopus.value) {
const quartile = item.Quartile || 'Unknown'
countByQuartile[quartile] = (countByQuartile[quartile] || 0) + 1
}
return {
labels: Object.keys(countByQuartile),
datasets: [
{
label: 'Jumlah Publikasi Scopus per Quartile',
backgroundColor: '#a78bfa', // ungu
data: Object.values(countByQuartile),
}
]
}
})
const BarChart = defineComponent({
name: 'BarChart',
props: ['data'],
setup(props) {
return () => h(Bar, { data: props.data, options: { responsive: true, plugins: { legend: { display: true }}} })
}
})
</script>
<style scoped>
.font-pixel {
font-family: 'Press Start 2P', cursive;
}
.pixel-border {
image-rendering: pixelated;
position: relative;
border-width: 4px;
border-style: solid;
border-radius: 8px;
box-shadow:
inset -2px -2px 0px rgba(0, 0, 0, 0.8),
inset 2px 2px 0px rgba(255, 255, 255, 0.2);
}
</style>