alterzick's picture
Add 3 files
a94002f verified
<!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>
<!-- Tabs Navigation -->
<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>
<!-- Input Tab -->
<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>
<!-- Import Excel Section -->
<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>
<!-- History Tab -->
<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>
<!-- Review Tab -->
<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>
<!-- Chart Tab -->
<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>
// Pagination variables
const ITEMS_PER_PAGE = 10;
let currentPage = 1;
// Default tab
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();
}
// Data Management
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!');
}
// Input Form Handling
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;
// Attachments processing
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,
// In a real app, this would be the URL to the uploaded file
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 = '';
});
// Handle attachment display
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);
}
}
});
// Format file size for display
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];
}
// Import from Excel
function importFromExcel() {
// Get elements
const fileInput = document.getElementById('excelFile');
const statusElement = document.getElementById('importStatus');
const importProgressText = document.getElementById('importProgressText');
const loadingSpinner = document.getElementById('loadingSpinner');
// Validate file selection
if (!fileInput.files.length) {
statusElement.textContent = 'Silakan pilih file terlebih dahulu.';
statusElement.className = 'text-red-500';
return;
}
// Show loading state
importProgressText.classList.add('hidden');
loadingSpinner.classList.remove('hidden');
// Reset status
statusElement.textContent = 'Memulai proses impor...';
statusElement.className = 'text-blue-500';
// Process file in background
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' });
// Get first worksheet
const worksheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[worksheetName];
// Convert to JSON
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;
}
// Process the data and add to localStorage
const jobs = getJobs();
let importedCount = 0;
// Define expected column mapping
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']
};
// Auto-detect columns
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 we couldn't detect columns from headers, assume standard order
if (Object.keys(columns).length < 2) {
columns.tanggal = 0;
columns.kegiatan = 1;
columns.status = 2;
columns.kategori = 3;
}
// Show status update for processing
statusElement.textContent = `Memproses ${jsonData.length} baris data...`;
// Process each row
const processNextRow = (index) => {
if (index >= jsonData.length) {
// All done - save and finish
saveJobs(jobs);
filterJobs();
filterReview();
filterChart();
statusElement.textContent = `Berhasil mengimpor ${importedCount} tugas dari file Excel.`;
statusElement.className = 'text-green-500';
// Reset file input
fileInput.value = '';
resetImportButton();
return;
}
// Process one row at a time with short delay to keep UI responsive
const row = jsonData[index];
statusElement.textContent = `Memproses data (${index + 1}/${jsonData.length})...`;
// Handle row processing based on whether we have headers
if (headerRow.length > 0) {
// Convert worksheet to array to access by index
const allRows = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const rowIndex = index + 1; // +1 for header
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 {
// No headers - use objects values
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++;
}
}
// Process next row after a short delay
setTimeout(() => processNextRow(index + 1), 10);
};
// Start processing rows
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); // Small delay to allow UI to update
}
// Reset import button to normal state
function resetImportButton() {
const importProgressText = document.getElementById('importProgressText');
const loadingSpinner = document.getElementById('loadingSpinner');
importProgressText.classList.remove('hidden');
loadingSpinner.classList.add('hidden');
}
// Format date from Excel to standard format
function formatDateForStorage(dateValue) {
// If it's already a string in yyyy-mm-dd format
if (typeof dateValue === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateValue)) {
return dateValue;
}
// If it's an Excel date serial number (common in .xlsx files)
if (typeof dateValue === 'number' && dateValue > 1) {
// Excel's base date is January 1, 1900, but there's a known bug where it treats 1900 as a leap year
// We need to adjust for this when converting
const is1900LeapBug = dateValue >= 60; // Dates after "February 29, 1900"
const correctedDateValue = dateValue - (is1900LeapBug ? 2 : 1); // Subtract 2 for the leap year bug and 1 for the day offset
const date = new Date(Date.UTC(1899, 11, 31 + correctedDateValue)); // Base date is December 31, 1899
return date.toISOString().split('T')[0];
}
// If it's a date object
if (dateValue instanceof Date) {
return dateValue.toISOString().split('T')[0];
}
// Try to parse as string
if (typeof dateValue === 'string') {
// Handle Indonesian date formats like "01/Jan/2023"
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];
}
}
}
// Handle other common date formats
const date = new Date(dateValue);
if (!isNaN(date.getTime())) {
return date.toISOString().split('T')[0];
}
}
return null;
}
// Filter Jobs (with search, date, and pagination)
function filterJobs() {
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
const filterDate = document.getElementById('filterDate').value;
const jobs = getJobs();
// Filter by search and date
const filtered = jobs.filter(job => {
const matchesSearch = job.task.toLowerCase().includes(searchQuery);
const matchesDate = filterDate ? job.date === filterDate : true;
return matchesSearch && matchesDate;
});
// Sort by date (newest first)
filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
// Pagination
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;
}
// Display paginated jobs
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);
});
// Generate pagination
generatePagination(totalPages, filtered.length);
}
// Generate pagination controls
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>';
// Previous button
paginationHTML += `<button onclick="changePage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''} class="px-2 py-1">&lt;</button>`;
// Page numbers
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>`;
}
// Next button
paginationHTML += `<button onclick="changePage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''} class="px-2 py-1">&gt;</button>`;
paginationContainer.innerHTML = paginationHTML;
}
// Change page
function changePage(page) {
currentPage = page;
filterJobs();
// Scroll to top of list
document.getElementById('historyList').scrollIntoView({ behavior: 'smooth' });
}
// Export to Excel from History
function exportToExcel() {
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
const filterDate = document.getElementById('filterDate').value;
const jobs = getJobs();
// Filter by search and date
let filtered = jobs.filter(job => {
const matchesSearch = job.task.toLowerCase().includes(searchQuery);
const matchesDate = filterDate ? job.date === filterDate : true;
return matchesSearch && matchesDate;
});
// Sort by date (newest first)
filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
// If no data, show alert
if (filtered.length === 0) {
alert('Tidak ada data untuk diekspor.');
return;
}
// Prepare data for export
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')
}));
// Create worksheet
const worksheet = XLSX.utils.json_to_sheet(exportData);
// Set column widths
const wscols = [
{wch: 12}, // Tanggal
{wch: 40}, // Kegiatan
{wch: 15}, // Status
{wch: 15}, // Kategori
{wch: 20} // Tanggal Input
];
worksheet['!cols'] = wscols;
// Create workbook
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Riwayat Tugas');
// Generate filename with current date
const dateStr = new Date().toISOString().slice(0, 10);
const filename = `Riwayat_Tugas_${dateStr}.xlsx`;
// Export
XLSX.writeFile(workbook, filename);
}
// Export to Excel from Review
function exportReviewToExcel() {
const statusFilter = document.getElementById('statusFilter').value;
const startDate = document.getElementById('reviewStartDate').value;
const endDate = document.getElementById('reviewEndDate').value;
const jobs = getJobs();
// Filter by status and date range
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;
});
// Sort by date (newest first)
filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
// If no data, show alert
if (filtered.length === 0) {
alert('Tidak ada data untuk diekspor.');
return;
}
// Prepare data for export
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')
}));
// Create worksheet
const worksheet = XLSX.utils.json_to_sheet(exportData);
// Set column widths
const wscols = [
{wch: 12}, // Tanggal
{wch: 40}, // Kegiatan
{wch: 15}, // Status
{wch: 15}, // Kategori
{wch: 20} // Tanggal Input
];
worksheet['!cols'] = wscols;
// Create workbook
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Review Tugas');
// Generate filename with current date and filters
const dateStr = new Date().toISOString().slice(0, 10);
let filename = `Review_Tugas_${dateStr}`;
if (statusFilter) filename += `_${statusFilter}`;
filename += '.xlsx';
// Export
XLSX.writeFile(workbook, filename);
}
// Format date for CSV
function formatDateCSV(dateStr) {
return new Date(dateStr).toLocaleDateString('id-ID');
}
// Remove attachment
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();
}
}
}
// Delete Job
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.');
}
}
// Filter Review
function filterReview() {
const statusFilter = document.getElementById('statusFilter').value;
const startDate = document.getElementById('reviewStartDate').value;
const endDate = document.getElementById('reviewEndDate').value;
const jobs = getJobs();
// Filter by status and date range
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;
});
// Sort by date (newest first)
filtered.sort((a, b) => new Date(b.date) - new Date(a.date));
const container = document.getElementById('reviewList');
container.innerHTML = '';
// Show active filters
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);
});
}
// Create filter badge
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);
}
// Filter Chart
function filterChart() {
const startDate = document.getElementById('chartStartDate').value;
const endDate = document.getElementById('chartEndDate').value;
const jobs = getJobs();
// Filter jobs within date range
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;
});
// Group by date
const dateMap = new Map();
// If no start/end date, show last 7 days by default
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) {
// Initialize all dates in range
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);
}
}
// Count completed jobs
filteredJobs.forEach(job => {
if (job.status === 'Selesai') {
const dateStr = job.date;
dateMap.set(dateStr, (dateMap.get(dateStr) || 0) + 1);
}
});
// Sort dates
const sortedDates = Array.from(dateMap.keys()).sort();
const labels = sortedDates.map(formatDate);
const data = sortedDates.map(date => dateMap.get(date));
// Destroy previous chart
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'
}
}
}
}
});
}
// Helper: Format date
function formatDate(dateStr) {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateStr).toLocaleDateString('id-ID', options);
}
// Auto-set today's date and initialize
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();
});
// Initial display
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>