| <!DOCTYPE html> |
| <html lang="id"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>Daily Job Tracker</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| body { |
| font-family: 'Inter', sans-serif; |
| } |
| .chart-container { |
| position: relative; |
| height: 300px; |
| width: 100%; |
| } |
| .filter-badge { |
| display: inline-flex; |
| align-items: center; |
| padding: 0.375rem 0.75rem; |
| border-radius: 9999px; |
| font-size: 0.875rem; |
| font-weight: 500; |
| margin-right: 0.5rem; |
| margin-bottom: 0.5rem; |
| } |
| .attachment-item { |
| display: flex; |
| align-items: center; |
| padding: 0.5rem; |
| background-color: #f7f7f7; |
| border-radius: 0.375rem; |
| margin-bottom: 0.5rem; |
| word-break: break-all; |
| } |
| .attachment-item a { |
| color: #4f46e5; |
| text-decoration: underline; |
| margin-left: 0.5rem; |
| } |
| .attachment-item button { |
| color: #ef4444; |
| margin-left: 0.5rem; |
| } |
| .pagination { |
| @apply flex justify-center mt-4 space-x-1; |
| } |
| .pagination button { |
| @apply px-3 py-1 border rounded text-sm hover:bg-gray-100; |
| } |
| .pagination button.active { |
| @apply bg-indigo-600 text-white; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 text-gray-800"> |
|
|
| <div class="max-w-5xl mx-auto p-5"> |
| <header class="text-center my-6"> |
| <h1 class="text-3xl font-bold text-indigo-600">📅 Daily Job Tracker</h1> |
| <p class="text-gray-600 mt-2">Lacak, review, dan pantau produktivitas harian Anda</p> |
| </header> |
|
|
| |
| <div class="flex border-b border-gray-300 mb-6 overflow-x-auto"> |
| <button onclick="showTab('input')" class="px-6 py-3 font-medium text-gray-700 border-b-2 border-indigo-500 bg-gray-100">➕ Input Job</button> |
| <button onclick="showTab('history')" class="px-6 py-3 text-gray-600 hover:text-indigo-600">📋 Riwayat</button> |
| <button onclick="showTab('review')" class="px-6 py-3 text-gray-600 hover:text-indigo-600">✅ Review</button> |
| <button onclick="showTab('chart')" class="px-6 py-3 text-gray-600 hover:text-indigo-600">📊 Grafik</button> |
| </div> |
|
|
| |
| <div id="input" class="tab-content"> |
| <div class="bg-white p-6 rounded-lg shadow mb-8"> |
| <h2 class="text-xl font-semibold mb-4">Catat Pekerjaan Hari Ini</h2> |
| <form id="jobForm" class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-600">Tanggal</label> |
| <input type="date" id="jobDate" class="mt-1 block w-full border border-gray-300 rounded-md p-2" required /> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600">Kegiatan</label> |
| <textarea id="jobTask" rows="3" class="mt-1 block w-full border border-gray-300 rounded-md p-2" placeholder="Tulis tugas yang telah dikerjakan..." required></textarea> |
| </div> |
| <div class="flex gap-4"> |
| <div class="w-1/2"> |
| <label class="block text-sm font-medium text-gray-600">Status</label> |
| <select id="jobStatus" class="mt-1 block w-full border border-gray-300 rounded-md p-2"> |
| <option value="Selesai">Selesai</option> |
| <option value="Dalam Proses">Dalam Proses</option> |
| <option value="Belum Mulai">Belum Mulai</option> |
| </select> |
| </div> |
| <div class="w-1/2"> |
| <label class="block text-sm font-medium text-gray-600">Kategori</label> |
| <select id="jobCategory" class="mt-1 block w-full border border-gray-300 rounded-md p-2"> |
| <option value="Pekerjaan">Pekerjaan</option> |
| <option value="Pribadi">Pribadi</option> |
| <option value="Belajar">Belajar</option> |
| <option value="Lainnya">Lainnya</option> |
| </select> |
| </div> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600">Lampiran</label> |
| <input type="file" id="jobAttachment" class="mt-1 block w-full border border-gray-300 rounded-md p-2" /> |
| <div id="attachmentList" class="mt-2"></div> |
| </div> |
| <button type="submit" class="bg-indigo-600 text-white px-5 py-2 rounded hover:bg-indigo-700 transition">➕ Tambahkan</button> |
| </form> |
| |
| |
| <div class="mt-8 p-6 border-t border-gray-200"> |
| <h3 class="text-lg font-semibold mb-4">📥 Impor Data dari Excel</h3> |
| <p class="text-gray-600 text-sm mb-4">Upload file Excel (.xlsx) dengan format kolom: Tanggal, Kegiatan, Status, Kategori</p> |
| <p class="text-blue-600 text-sm mb-4">Anda dapat mengimpor data tanpa batas. Semua data akan ditambahkan ke dalam sistem.</p> |
| <div class="flex items-center"> |
| <input type="file" id="excelFile" accept=".xlsx" class="p-2 border border-gray-300 rounded flex-1" /> |
| <button onclick="importFromExcel()" class="ml-2 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"> |
| <span id="importProgressText">Import</span> |
| <span id="loadingSpinner" class="hidden"> |
| <svg class="inline w-5 h-5 mr-2 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
| </svg> |
| Memproses... |
| </span> |
| </button> |
| </div> |
| <p id="importStatus" class="mt-2 text-sm"></p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="history" class="tab-content hidden"> |
| <div class="bg-white p-6 rounded-lg shadow mb-8"> |
| <h2 class="text-xl font-semibold mb-4">📅 Riwayat Pekerjaan</h2> |
| <div class="flex flex-col sm:flex-row gap-4 mb-4 items-start"> |
| <input type="text" id="searchInput" placeholder="Cari tugas..." class="p-2 border border-gray-300 rounded flex-1" oninput="filterJobs()" /> |
| <input type="date" id="filterDate" class="p-2 border border-gray-300 rounded w-full sm:w-48" oninput="filterJobs()" /> |
| <button onclick="exportToExcel()" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 whitespace-nowrap"> |
| 📤 Ekspor ke Excel |
| </button> |
| </div> |
| <div id="historyList" class="space-y-3 max-h-96 overflow-y-auto"></div> |
| <div id="paginationContainer" class="pagination mt-4"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="review" class="tab-content hidden"> |
| <div class="bg-white p-6 rounded-lg shadow mb-8"> |
| <h2 class="text-xl font-semibold mb-4">✅ Review Harian</h2> |
| <p class="text-gray-600 mb-4">Tinjau tugas-tugas Anda berdasarkan status.</p> |
| <div class="flex flex-col sm:flex-row gap-4 mb-4"> |
| <select id="statusFilter" class="p-2 border border-gray-300 rounded flex-1" onchange="filterReview()"> |
| <option value="">Semua Status</option> |
| <option value="Selesai">Selesai</option> |
| <option value="Dalam Proses">Dalam Proses</option> |
| <option value="Belum Mulai">Belum Mulai</option> |
| </select> |
| <input type="date" id="reviewStartDate" class="p-2 border border-gray-300 rounded w-full sm:w-32" oninput="filterReview()" /> |
| <span class="self-center">sampai</span> |
| <input type="date" id="reviewEndDate" class="p-2 border border-gray-300 rounded w-full sm:w-32" oninput="filterReview()" /> |
| <button onclick="exportReviewToExcel()" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 whitespace-nowrap"> |
| 📤 Ekspor ke Excel |
| </button> |
| </div> |
| <div class="flex flex-wrap mb-4"> |
| <div id="activeFilters" class="flex flex-wrap"></div> |
| </div> |
| <div id="reviewList" class="space-y-3"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="chart" class="tab-content hidden"> |
| <div class="bg-white p-6 rounded-lg shadow mb-8"> |
| <h2 class="text-xl font-semibold mb-4">📊 Grafik Produktivitas</h2> |
| <p class="text-gray-600 mb-4">Analisis produktivitas Anda dalam rentang tanggal tertentu.</p> |
| <div class="flex flex-col sm:flex-row gap-4 mb-6"> |
| <input type="date" id="chartStartDate" class="p-2 border border-gray-300 rounded w-full sm:w-1/2" oninput="filterChart()" /> |
| <span class="self-center">sampai</span> |
| <input type="date" id="chartEndDate" class="p-2 border border-gray-300 rounded w-full sm:w-1/2" oninput="filterChart()" /> |
| </div> |
| <div class="chart-container"> |
| <canvas id="productivityChart"></canvas> |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| <script> |
| |
| const ITEMS_PER_PAGE = 10; |
| let currentPage = 1; |
| |
| |
| function showTab(tabId) { |
| document.querySelectorAll('.tab-content').forEach(tab => { |
| tab.classList.add('hidden'); |
| }); |
| document.getElementById(tabId).classList.remove('hidden'); |
| if (tabId === 'chart') filterChart(); |
| if (tabId === 'history') { |
| currentPage = 1; |
| filterJobs(); |
| } |
| if (tabId === 'review') filterReview(); |
| } |
| |
| |
| function getJobs() { |
| return JSON.parse(localStorage.getItem('dailyJobs') || '[]'); |
| } |
| |
| function saveJobs(jobs) { |
| localStorage.setItem('dailyJobs', JSON.stringify(jobs)); |
| } |
| |
| function addJob(job) { |
| const jobs = getJobs(); |
| jobs.push(job); |
| saveJobs(jobs); |
| filterJobs(); |
| filterReview(); |
| filterChart(); |
| alert('Tugas berhasil ditambahkan!'); |
| } |
| |
| |
| document.getElementById('jobForm').addEventListener('submit', function(e) { |
| e.preventDefault(); |
| const date = document.getElementById('jobDate').value; |
| const task = document.getElementById('jobTask').value; |
| const status = document.getElementById('jobStatus').value; |
| const category = document.getElementById('jobCategory').value; |
| |
| |
| const attachmentInput = document.getElementById('jobAttachment'); |
| const attachments = []; |
| |
| if (attachmentInput.files.length > 0) { |
| for (let i = 0; i < attachmentInput.files.length; i++) { |
| const file = attachmentInput.files[i]; |
| attachments.push({ |
| name: file.name, |
| type: file.type, |
| size: file.size, |
| |
| url: `data/${file.name}` |
| }); |
| } |
| } |
| |
| const job = { |
| id: Date.now(), |
| date, |
| task, |
| status, |
| category, |
| attachments: attachments, |
| timestamp: new Date().toISOString() |
| }; |
| |
| addJob(job); |
| this.reset(); |
| document.getElementById('attachmentList').innerHTML = ''; |
| }); |
| |
| |
| document.getElementById('jobAttachment').addEventListener('change', function(e) { |
| const attachmentList = document.getElementById('attachmentList'); |
| attachmentList.innerHTML = ''; |
| |
| if (this.files.length > 0) { |
| for (let i = 0; i < this.files.length; i++) { |
| const file = this.files[i]; |
| const attachmentItem = document.createElement('div'); |
| attachmentItem.className = 'attachment-item'; |
| attachmentItem.innerHTML = ` |
| <span>📎 ${file.name} (${formatFileSize(file.size)})</span> |
| `; |
| attachmentList.appendChild(attachmentItem); |
| } |
| } |
| }); |
| |
| |
| function formatFileSize(bytes) { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| } |
| |
| |
| function importFromExcel() { |
| |
| const fileInput = document.getElementById('excelFile'); |
| const statusElement = document.getElementById('importStatus'); |
| const importProgressText = document.getElementById('importProgressText'); |
| const loadingSpinner = document.getElementById('loadingSpinner'); |
| |
| |
| if (!fileInput.files.length) { |
| statusElement.textContent = 'Silakan pilih file terlebih dahulu.'; |
| statusElement.className = 'text-red-500'; |
| return; |
| } |
| |
| |
| importProgressText.classList.add('hidden'); |
| loadingSpinner.classList.remove('hidden'); |
| |
| |
| statusElement.textContent = 'Memulai proses impor...'; |
| statusElement.className = 'text-blue-500'; |
| |
| |
| setTimeout(() => { |
| const file = fileInput.files[0]; |
| const reader = new FileReader(); |
| |
| reader.onload = function(e) { |
| try { |
| const data = new Uint8Array(e.target.result); |
| const workbook = XLSX.read(data, { type: 'array' }); |
| |
| |
| const worksheetName = workbook.SheetNames[0]; |
| const worksheet = workbook.Sheets[worksheetName]; |
| |
| |
| const jsonData = XLSX.utils.sheet_to_json(worksheet); |
| |
| if (jsonData.length === 0) { |
| statusElement.textContent = 'Tidak ada data ditemukan dalam file Excel.'; |
| statusElement.className = 'text-yellow-500'; |
| resetImportButton(); |
| return; |
| } |
| |
| |
| const jobs = getJobs(); |
| let importedCount = 0; |
| |
| |
| const columnMapping = { |
| 'tanggal': ['tanggal', 'date', 'Tanggal', 'Date', 'TGL', 'tgl', 'created', 'Created'], |
| 'kegiatan': ['kegiatan', 'task', 'Kegiatan', 'Task', 'Tugas', 'tugas', 'Description', 'description', 'Content', 'content'], |
| 'status': ['status', 'Status', 'STATUS', 'Completion', 'completion'], |
| 'kategori': ['kategori', 'category', 'Kategori', 'Category', 'Jenis', 'jenis', 'Type', 'type', 'Class', 'class'] |
| }; |
| |
| |
| const headerRow = XLSX.utils.sheet_to_json(worksheet, { header: 1 })[0]; |
| const columns = {}; |
| |
| if (headerRow) { |
| headerRow.forEach((header, index) => { |
| const headerStr = header.toString().trim(); |
| for (const [standardName, possibleNames] of Object.entries(columnMapping)) { |
| if (possibleNames.includes(headerStr)) { |
| columns[standardName] = index; |
| break; |
| } |
| } |
| }); |
| } |
| |
| |
| if (Object.keys(columns).length < 2) { |
| columns.tanggal = 0; |
| columns.kegiatan = 1; |
| columns.status = 2; |
| columns.kategori = 3; |
| } |
| |
| |
| statusElement.textContent = `Memproses ${jsonData.length} baris data...`; |
| |
| |
| const processNextRow = (index) => { |
| if (index >= jsonData.length) { |
| |
| saveJobs(jobs); |
| filterJobs(); |
| filterReview(); |
| filterChart(); |
| |
| statusElement.textContent = `Berhasil mengimpor ${importedCount} tugas dari file Excel.`; |
| statusElement.className = 'text-green-500'; |
| |
| |
| fileInput.value = ''; |
| resetImportButton(); |
| return; |
| } |
| |
| |
| const row = jsonData[index]; |
| statusElement.textContent = `Memproses data (${index + 1}/${jsonData.length})...`; |
| |
| |
| if (headerRow.length > 0) { |
| |
| const allRows = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); |
| const rowIndex = index + 1; |
| if (rowIndex < allRows.length) { |
| const rowArray = allRows[rowIndex]; |
| const date = formatDateForStorage(rowArray[columns.tanggal]); |
| const task = rowArray[columns.kegiatan]; |
| const status = rowArray[columns.status] || 'Belum Mulai'; |
| const category = rowArray[columns.kategori] || 'Lainnya'; |
| |
| if (date && task) { |
| jobs.push({ |
| id: Date.now() + importedCount, |
| date, |
| task, |
| status, |
| category, |
| timestamp: new Date().toISOString() |
| }); |
| importedCount++; |
| } |
| } |
| } else { |
| |
| const values = Object.values(row); |
| const date = formatDateForStorage(values[columns.tanggal]); |
| const task = values[columns.kegiatan]; |
| const status = values[columns.status] || 'Belum Mulai'; |
| const category = values[columns.kategori] || 'Lainnya'; |
| |
| if (date && task) { |
| jobs.push({ |
| id: Date.now() + importedCount, |
| date, |
| task, |
| status, |
| category, |
| timestamp: new Date().toISOString() |
| }); |
| importedCount++; |
| } |
| } |
| |
| |
| setTimeout(() => processNextRow(index + 1), 10); |
| }; |
| |
| |
| processNextRow(0); |
| |
| } catch (error) { |
| console.error("Error processing Excel file:", error); |
| statusElement.textContent = `Gagal membaca file Excel: ${error.message}`; |
| statusElement.className = 'text-red-500'; |
| resetImportButton(); |
| } |
| }; |
| |
| reader.onerror = function() { |
| statusElement.textContent = 'Gagal membaca file.'; |
| statusElement.className = 'text-red-500'; |
| resetImportButton(); |
| }; |
| |
| reader.readAsArrayBuffer(file); |
| }, 100); |
| } |
| |
| |
| function resetImportButton() { |
| const importProgressText = document.getElementById('importProgressText'); |
| const loadingSpinner = document.getElementById('loadingSpinner'); |
| importProgressText.classList.remove('hidden'); |
| loadingSpinner.classList.add('hidden'); |
| } |
| |
| |
| function formatDateForStorage(dateValue) { |
| |
| if (typeof dateValue === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateValue)) { |
| return dateValue; |
| } |
| |
| |
| if (typeof dateValue === 'number' && dateValue > 1) { |
| |
| |
| const is1900LeapBug = dateValue >= 60; |
| const correctedDateValue = dateValue - (is1900LeapBug ? 2 : 1); |
| const date = new Date(Date.UTC(1899, 11, 31 + correctedDateValue)); |
| return date.toISOString().split('T')[0]; |
| } |
| |
| |
| if (dateValue instanceof Date) { |
| return dateValue.toISOString().split('T')[0]; |
| } |
| |
| |
| if (typeof dateValue === 'string') { |
| |
| const idDateRegex = /^(\d{1,2})\/([A-Za-z]{3})\/(\d{4})$/; |
| if (idDateRegex.test(dateValue)) { |
| const months = { |
| 'Jan': 0, 'Feb': 1, 'Mar': 2, 'Apr': 3, 'Mei': 4, 'Jun': 5, |
| 'Jul': 6, 'Agu': 7, 'Sep': 8, 'Okt': 9, 'Nov': 10, 'Des': 11 |
| }; |
| |
| const match = dateValue.match(idDateRegex); |
| const day = parseInt(match[1]); |
| const month = months[match[2]]; |
| const year = parseInt(match[3]); |
| |
| if (month !== undefined) { |
| const date = new Date(year, month, day); |
| if (!isNaN(date.getTime())) { |
| return date.toISOString().split('T')[0]; |
| } |
| } |
| } |
| |
| |
| const date = new Date(dateValue); |
| if (!isNaN(date.getTime())) { |
| return date.toISOString().split('T')[0]; |
| } |
| } |
| |
| return null; |
| } |
| |
| |
| function filterJobs() { |
| const searchQuery = document.getElementById('searchInput').value.toLowerCase(); |
| const filterDate = document.getElementById('filterDate').value; |
| const jobs = getJobs(); |
| |
| |
| const filtered = jobs.filter(job => { |
| const matchesSearch = job.task.toLowerCase().includes(searchQuery); |
| const matchesDate = filterDate ? job.date === filterDate : true; |
| return matchesSearch && matchesDate; |
| }); |
| |
| |
| filtered.sort((a, b) => new Date(b.date) - new Date(a.date)); |
| |
| |
| const totalPages = Math.ceil(filtered.length / ITEMS_PER_PAGE); |
| currentPage = Math.min(currentPage, totalPages || 1); |
| |
| const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; |
| const paginatedJobs = filtered.slice(startIndex, startIndex + ITEMS_PER_PAGE); |
| |
| const container = document.getElementById('historyList'); |
| container.innerHTML = ''; |
| |
| if (filtered.length === 0) { |
| container.innerHTML = '<p class="text-gray-500">Tidak ada tugas ditemukan berdasarkan filter yang diterapkan.</p>'; |
| document.getElementById('paginationContainer').innerHTML = ''; |
| return; |
| } |
| |
| |
| paginatedJobs.forEach(job => { |
| const el = document.createElement('div'); |
| el.className = 'p-4 border border-gray-200 rounded bg-gray-50'; |
| el.innerHTML = ` |
| <div class="flex justify-between"> |
| <strong>${formatDate(job.date)} - ${job.category}</strong> |
| <span class="px-2 py-1 text-xs rounded-full ${ |
| job.status === 'Selesai' ? 'bg-green-200 text-green-800' : |
| job.status === 'Dalam Proses' ? 'bg-yellow-200 text-yellow-800' : |
| 'bg-gray-200 text-gray-800' |
| }">${job.status}</span> |
| </div> |
| <p class="mt-2">${job.task}</p> |
| ${job.attachments && job.attachments.length > 0 ? |
| `<div class="mt-2"> |
| <strong class="text-sm">Lampiran:</strong> |
| <div class="space-y-1 mt-1"> |
| ${job.attachments.map(att => ` |
| <div class="attachment-item"> |
| <span>📎 ${att.name}</span> |
| <a href="${att.url}" target="_blank" download>Unduh</a> |
| <button onclick="removeAttachment(${job.id}, '${att.name}')">❌</button> |
| </div> |
| `).join('')} |
| </div> |
| </div>` : ''} |
| <button onclick="deleteJob(${job.id})" class="text-red-500 text-sm mt-2 hover:underline">❌ Hapus</button> |
| `; |
| container.appendChild(el); |
| }); |
| |
| |
| generatePagination(totalPages, filtered.length); |
| } |
| |
| |
| function generatePagination(totalPages, totalItems) { |
| const paginationContainer = document.getElementById('paginationContainer'); |
| |
| if (totalPages <= 1) { |
| paginationContainer.innerHTML = ''; |
| return; |
| } |
| |
| let paginationHTML = '<div class="text-sm text-gray-600 mr-4">Total: ' + totalItems + ' tugas</div>'; |
| |
| |
| paginationHTML += `<button onclick="changePage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''} class="px-2 py-1"><</button>`; |
| |
| |
| const startPage = Math.max(1, currentPage - 2); |
| const endPage = Math.min(totalPages, startPage + 4); |
| |
| for (let i = startPage; i <= endPage; i++) { |
| paginationHTML += `<button onclick="changePage(${i})" class="${i === currentPage ? 'bg-indigo-600 text-white' : 'bg-white'}">${i}</button>`; |
| } |
| |
| |
| paginationHTML += `<button onclick="changePage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''} class="px-2 py-1">></button>`; |
| |
| paginationContainer.innerHTML = paginationHTML; |
| } |
| |
| |
| function changePage(page) { |
| currentPage = page; |
| filterJobs(); |
| |
| |
| document.getElementById('historyList').scrollIntoView({ behavior: 'smooth' }); |
| } |
| |
| |
| function exportToExcel() { |
| const searchQuery = document.getElementById('searchInput').value.toLowerCase(); |
| const filterDate = document.getElementById('filterDate').value; |
| const jobs = getJobs(); |
| |
| |
| let filtered = jobs.filter(job => { |
| const matchesSearch = job.task.toLowerCase().includes(searchQuery); |
| const matchesDate = filterDate ? job.date === filterDate : true; |
| return matchesSearch && matchesDate; |
| }); |
| |
| |
| filtered.sort((a, b) => new Date(b.date) - new Date(a.date)); |
| |
| |
| if (filtered.length === 0) { |
| alert('Tidak ada data untuk diekspor.'); |
| return; |
| } |
| |
| |
| const exportData = filtered.map(job => ({ |
| 'Tanggal': formatDateCSV(job.date), |
| 'Kegiatan': job.task, |
| 'Status': job.status, |
| 'Kategori': job.category, |
| 'Tanggal Input': new Date(job.timestamp).toLocaleString('id-ID') |
| })); |
| |
| |
| const worksheet = XLSX.utils.json_to_sheet(exportData); |
| |
| |
| const wscols = [ |
| {wch: 12}, |
| {wch: 40}, |
| {wch: 15}, |
| {wch: 15}, |
| {wch: 20} |
| ]; |
| worksheet['!cols'] = wscols; |
| |
| |
| const workbook = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(workbook, worksheet, 'Riwayat Tugas'); |
| |
| |
| const dateStr = new Date().toISOString().slice(0, 10); |
| const filename = `Riwayat_Tugas_${dateStr}.xlsx`; |
| |
| |
| XLSX.writeFile(workbook, filename); |
| } |
| |
| |
| function exportReviewToExcel() { |
| const statusFilter = document.getElementById('statusFilter').value; |
| const startDate = document.getElementById('reviewStartDate').value; |
| const endDate = document.getElementById('reviewEndDate').value; |
| const jobs = getJobs(); |
| |
| |
| let filtered = jobs.filter(job => { |
| const matchesStatus = statusFilter ? job.status === statusFilter : true; |
| const jobDate = new Date(job.date); |
| const afterStart = startDate ? jobDate >= new Date(startDate) : true; |
| const beforeEnd = endDate ? jobDate <= new Date(endDate) : true; |
| return matchesStatus && afterStart && beforeEnd; |
| }); |
| |
| |
| filtered.sort((a, b) => new Date(b.date) - new Date(a.date)); |
| |
| |
| if (filtered.length === 0) { |
| alert('Tidak ada data untuk diekspor.'); |
| return; |
| } |
| |
| |
| const exportData = filtered.map(job => ({ |
| 'Tanggal': formatDateCSV(job.date), |
| 'Kegiatan': job.task, |
| 'Status': job.status, |
| 'Kategori': job.category, |
| 'Tanggal Input': new Date(job.timestamp).toLocaleString('id-ID') |
| })); |
| |
| |
| const worksheet = XLSX.utils.json_to_sheet(exportData); |
| |
| |
| const wscols = [ |
| {wch: 12}, |
| {wch: 40}, |
| {wch: 15}, |
| {wch: 15}, |
| {wch: 20} |
| ]; |
| worksheet['!cols'] = wscols; |
| |
| |
| const workbook = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(workbook, worksheet, 'Review Tugas'); |
| |
| |
| const dateStr = new Date().toISOString().slice(0, 10); |
| let filename = `Review_Tugas_${dateStr}`; |
| if (statusFilter) filename += `_${statusFilter}`; |
| filename += '.xlsx'; |
| |
| |
| XLSX.writeFile(workbook, filename); |
| } |
| |
| |
| function formatDateCSV(dateStr) { |
| return new Date(dateStr).toLocaleDateString('id-ID'); |
| } |
| |
| |
| function removeAttachment(jobId, attachmentName) { |
| if (confirm(`Hapus lampiran ${attachmentName}?`)) { |
| let jobs = getJobs(); |
| const jobIndex = jobs.findIndex(j => j.id === jobId); |
| |
| if (jobIndex !== -1) { |
| jobs[jobIndex].attachments = jobs[jobIndex].attachments.filter( |
| att => att.name !== attachmentName |
| ); |
| |
| saveJobs(jobs); |
| filterJobs(); |
| } |
| } |
| } |
| |
| |
| function deleteJob(id) { |
| if (confirm('Anda yakin ingin menghapus tugas ini?')) { |
| let jobs = getJobs(); |
| jobs = jobs.filter(job => job.id !== id); |
| saveJobs(jobs); |
| filterJobs(); |
| filterReview(); |
| filterChart(); |
| alert('Tugas dihapus.'); |
| } |
| } |
| |
| |
| function filterReview() { |
| const statusFilter = document.getElementById('statusFilter').value; |
| const startDate = document.getElementById('reviewStartDate').value; |
| const endDate = document.getElementById('reviewEndDate').value; |
| const jobs = getJobs(); |
| |
| |
| const filtered = jobs.filter(job => { |
| const matchesStatus = statusFilter ? job.status === statusFilter : true; |
| const jobDate = new Date(job.date); |
| const afterStart = startDate ? jobDate >= new Date(startDate) : true; |
| const beforeEnd = endDate ? jobDate <= new Date(endDate) : true; |
| return matchesStatus && afterStart && beforeEnd; |
| }); |
| |
| |
| filtered.sort((a, b) => new Date(b.date) - new Date(a.date)); |
| |
| const container = document.getElementById('reviewList'); |
| container.innerHTML = ''; |
| |
| |
| const activeFiltersContainer = document.getElementById('activeFilters'); |
| activeFiltersContainer.innerHTML = ''; |
| if (statusFilter) { |
| createFilterBadge(activeFiltersContainer, `Status: ${statusFilter}`); |
| } |
| if (startDate) { |
| createFilterBadge(activeFiltersContainer, `Dari: ${formatDate(startDate)}`); |
| } |
| if (endDate) { |
| createFilterBadge(activeFiltersContainer, `Hingga: ${formatDate(endDate)}`); |
| } |
| |
| if (filtered.length === 0) { |
| container.innerHTML = '<p class="text-gray-500">Tidak ada tugas sesuai dengan filter yang diterapkan.</p>'; |
| return; |
| } |
| |
| filtered.forEach(job => { |
| const el = document.createElement('div'); |
| el.className = 'p-4 border border-gray-200 rounded bg-white'; |
| el.innerHTML = ` |
| <div class="flex justify-between items-start"> |
| <div> |
| <strong>${formatDate(job.date)} - ${job.category}</strong> |
| <p class="mt-1 text-gray-700">${job.task}</p> |
| ${job.attachments && job.attachments.length > 0 ? |
| `<div class="mt-1"> |
| <span class="text-xs">📎 ${job.attachments.length} lampiran</span> |
| </div>` : ''} |
| </div> |
| <span class="px-2 py-1 text-xs rounded-full ${ |
| job.status === 'Selesai' ? 'bg-green-200 text-green-800' : |
| job.status === 'Dalam Proses' ? 'bg-yellow-200 text-yellow-800' : |
| 'bg-gray-200 text-gray-800' |
| }">${job.status}</span> |
| </div> |
| `; |
| container.appendChild(el); |
| }); |
| } |
| |
| |
| function createFilterBadge(container, text) { |
| const badge = document.createElement('span'); |
| badge.className = `filter-badge ${ |
| text.startsWith('Status') ? 'bg-indigo-100 text-indigo-800' : |
| text.startsWith('Dari') ? 'bg-blue-100 text-blue-800' : |
| 'bg-purple-100 text-purple-800' |
| }`; |
| badge.textContent = text; |
| container.appendChild(badge); |
| } |
| |
| |
| function filterChart() { |
| const startDate = document.getElementById('chartStartDate').value; |
| const endDate = document.getElementById('chartEndDate').value; |
| const jobs = getJobs(); |
| |
| |
| const filteredJobs = jobs.filter(job => { |
| const jobDate = new Date(job.date); |
| const afterStart = startDate ? jobDate >= new Date(startDate) : true; |
| const beforeEnd = endDate ? jobDate <= new Date(endDate) : true; |
| return afterStart && beforeEnd; |
| }); |
| |
| |
| const dateMap = new Map(); |
| |
| |
| if (!startDate && !endDate) { |
| const end = new Date(); |
| const start = new Date(); |
| start.setDate(end.getDate() - 6); |
| |
| let current = new Date(start); |
| while (current <= end) { |
| const dateStr = current.toISOString().split('T')[0]; |
| dateMap.set(dateStr, 0); |
| current.setDate(current.getDate() + 1); |
| } |
| } else if (startDate && endDate) { |
| |
| const start = new Date(startDate); |
| const end = new Date(endDate); |
| let current = new Date(start); |
| while (current <= end) { |
| const dateStr = current.toISOString().split('T')[0]; |
| dateMap.set(dateStr, 0); |
| current.setDate(current.getDate() + 1); |
| } |
| } |
| |
| |
| filteredJobs.forEach(job => { |
| if (job.status === 'Selesai') { |
| const dateStr = job.date; |
| dateMap.set(dateStr, (dateMap.get(dateStr) || 0) + 1); |
| } |
| }); |
| |
| |
| const sortedDates = Array.from(dateMap.keys()).sort(); |
| const labels = sortedDates.map(formatDate); |
| const data = sortedDates.map(date => dateMap.get(date)); |
| |
| |
| if (window.dailyChart) { |
| window.dailyChart.destroy(); |
| } |
| |
| const ctx = document.getElementById('productivityChart').getContext('2d'); |
| window.dailyChart = new Chart(ctx, { |
| type: 'bar', |
| data: { |
| labels: labels, |
| datasets: [{ |
| label: 'Jumlah Tugas Selesai', |
| data: data, |
| backgroundColor: 'rgba(79, 70, 229, 0.7)', |
| borderColor: 'rgba(79, 70, 229, 1)', |
| borderWidth: 1 |
| }] |
| }, |
| options: { |
| responsive: true, |
| plugins: { |
| tooltip: { |
| mode: 'index', |
| intersect: false, |
| }, |
| legend: { |
| display: true, |
| position: 'top' |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: true, |
| ticks: { |
| stepSize: 1 |
| }, |
| title: { |
| display: true, |
| text: 'Jumlah Tugas Selesai' |
| } |
| }, |
| x: { |
| title: { |
| display: true, |
| text: 'Tanggal' |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| function formatDate(dateStr) { |
| const options = { year: 'numeric', month: 'short', day: 'numeric' }; |
| return new Date(dateStr).toLocaleDateString('id-ID', options); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const today = new Date(); |
| const formattedToday = today.toISOString().split('T')[0]; |
| |
| document.getElementById('jobDate').value = formattedToday; |
| document.getElementById('filterDate').value = formattedToday; |
| document.getElementById('reviewStartDate').value = formattedToday; |
| document.getElementById('reviewEndDate').value = formattedToday; |
| document.getElementById('chartStartDate').value = formattedToday; |
| document.getElementById('chartEndDate').value = formattedToday; |
| |
| filterJobs(); |
| filterReview(); |
| filterChart(); |
| }); |
| |
| |
| showTab('input'); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/https-huggingface-co-spaces-alterzick-daily-job-tracker-v1" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |