MBWA / index.html
jhaspr's picture
Show only task strictly on the left side and details on the right side - Follow Up Deployment
8a28b78 verified
Raw History Blame Contribute Delete
36.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MBWA (Management By Wandering Around)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
#imageModal img {
cursor: zoom-out;
}
.task-card {
transition: all 0.2s ease;
}
.task-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.comment-box {
max-height: 200px;
overflow-y: auto;
scrollbar-width: thin;
}
.comment-box::-webkit-scrollbar {
width: 4px;
}
.comment-box::-webkit-scrollbar-track {
background: #f1f1f1;
}
.comment-box::-webkit-scrollbar-thumb {
background: #888;
border-radius: 2px;
}
.comment-box::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body class="min-h-screen bg-gray-50">
<div class="max-w-5xl mx-auto p-6">
<header class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">MBWA (Management By Wandering Around)</h1>
<p class="text-gray-600 mt-1">Track tasks, observations and improvements from your management walks.</p>
</header>
<section class="bg-white border rounded-2xl p-6 shadow-sm mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Create Task</h2>
<form id="taskForm" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Task</label>
<input
type="text"
id="taskTitle"
placeholder="What needs to be done?"
class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p id="titleError" class="text-sm text-red-600 mt-1 hidden"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Location</label>
<select
id="taskLocation"
class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select location</option>
<option value="Berthaphil I">Berthaphil I</option>
<option value="Berthaphil II South">Berthaphil II South</option>
<option value="Berthaphil II North">Berthaphil II North</option>
<option value="Berthaphil Office Park">Berthaphil Office Park</option>
<option value="Clark Center">Clark Center</option>
<option value="Berthaphil IV">Berthaphil IV</option>
<option value="Berthaphil VI">Berthaphil VI</option>
<option value="Berthaphil VIII">Berthaphil VIII</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Assigned to</label>
<input
type="text"
id="assignedTo"
placeholder="Name"
class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p id="assigneeError" class="text-sm text-red-600 mt-1 hidden"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date reported</label>
<input
type="date"
id="dateReported"
class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p id="dateError" class="text-sm text-red-600 mt-1 hidden"></p>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Image (optional)</label>
<div class="flex items-center gap-4">
<label class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 cursor-pointer text-sm font-medium text-gray-700">
<input type="file" id="taskImage" accept="image/*" class="hidden">
Upload image
</label>
<div id="imagePreview" class="bg-gray-200 border-2 border-dashed rounded-xl w-16 h-16"></div>
<button
type="button"
id="removeImageBtn"
class="text-sm text-gray-600 hover:text-gray-800 underline hidden"
>
Remove
</button>
</div>
</div>
<div class="md:col-span-3">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Add Task
</button>
</div>
</form>
</section>
<section>
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900">Tasks</h2>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-700">Status:</label>
<select
id="statusFilter"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="All">All</option>
<option value="Pending">Pending</option>
<option value="In-Progress">In-Progress</option>
<option value="Completed">Completed</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-700">Assigned:</label>
<select
id="assigneeFilter"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="All">All</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-700">Location:</label>
<select
id="locationFilter"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="All">All</option>
<option value="Berthaphil I">Berthaphil I</option>
<option value="Berthaphil II South">Berthaphil II South</option>
<option value="Berthaphil II North">Berthaphil II North</option>
<option value="Berthaphil Office Park">Berthaphil Office Park</option>
<option value="Clark Center">Clark Center</option>
<option value="Berthaphil IV">Berthaphil IV</option>
<option value="Berthaphil VI">Berthaphil VI</option>
<option value="Berthaphil VIII">Berthaphil VIII</option>
</select>
</div>
</div>
</div>
<div id="emptyState" class="bg-white border rounded-2xl p-6 text-center text-gray-600 hidden">
No tasks yet. Create your first task above.
</div>
<div id="tasksContainer" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
</section>
</div>
<!-- Image Modal -->
<div id="imageModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden">
<div class="relative max-w-4xl max-h-screen p-4">
<button onclick="document.getElementById('imageModal').classList.add('hidden')" class="absolute top-4 right-4 text-white text-2xl z-10">×</button>
<img id="modalImage" class="max-w-full max-h-screen" src="" alt="Enlarged view">
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/lz-string@1.4.4/libs/lz-string.min.js"></script>
<script>
// Show image modal
function showImageModal(src) {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
modalImg.src = src;
modal.classList.remove('hidden');
}
// Close modal when clicking outside image
document.getElementById('imageModal').addEventListener('click', function(e) {
if (e.target === this) {
this.classList.add('hidden');
}
});
// Status options and badge classes
const STATUS_OPTIONS = ["Pending", "In-Progress", "Completed"];
function statusBadgeClasses(status) {
switch (status) {
case "Pending": return "bg-gray-100 text-gray-700 border-gray-200";
case "In-Progress": return "bg-blue-100 text-blue-700 border-blue-200";
case "Completed": return "bg-green-100 text-green-700 border-green-200";
default: return "bg-gray-100 text-gray-700 border-gray-200";
}
}
// Format date from YYYY-MM-DD to locale string
function formatDate(dateStr) {
if (!dateStr) return "";
const [y, m, d] = dateStr.split("-").map(Number);
const dt = new Date(y || 0, (m || 1) - 1, d || 1);
return dt.toLocaleDateString();
}
// Task storage
let tasks = JSON.parse(localStorage.getItem("crm_tasks_v1")) || [];
// Handle initial image upload
function handleInitialImageUpload(taskId, input) {
const file = input.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
alert("Please select an image file");
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const task = tasks.find(t => t.id === taskId);
if (task) {
task.imageDataUrl = event.target.result;
saveTasks();
renderTasks();
}
};
reader.readAsDataURL(file);
}
// Handle completed image upload
function handleCompletedImageUpload(taskId, input) {
const file = input.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
alert("Please select an image file");
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const task = tasks.find(t => t.id === taskId);
if (task) {
task.completedImageDataUrl = event.target.result;
// Add a comment about the photo upload
const newComment = {
id: "c_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
text: "Uploaded completed task photo",
createdAt: new Date().toISOString()
};
task.comments.unshift(newComment);
saveTasks();
renderTasks();
}
};
reader.readAsDataURL(file);
}
// DOM elements
const taskForm = document.getElementById("taskForm");
const taskTitle = document.getElementById("taskTitle");
const assignedTo = document.getElementById("assignedTo");
const dateReported = document.getElementById("dateReported");
const taskImage = document.getElementById("taskImage");
const imagePreview = document.getElementById("imagePreview");
const removeImageBtn = document.getElementById("removeImageBtn");
const statusFilter = document.getElementById("statusFilter");
const assigneeFilter = document.getElementById("assigneeFilter");
const tasksContainer = document.getElementById("tasksContainer");
const emptyState = document.getElementById("emptyState");
// Error elements
const titleError = document.getElementById("titleError");
const assigneeError = document.getElementById("assigneeError");
const dateError = document.getElementById("dateError");
// Initialize
function init() {
renderTasks();
setupEventListeners();
feather.replace();
}
// Set up event listeners
function setupEventListeners() {
taskForm.addEventListener("submit", handleAddTask);
taskImage.addEventListener("change", handleFileChange);
removeImageBtn.addEventListener("click", () => {
taskImage.value = "";
imagePreview.innerHTML = "";
imagePreview.className = "bg-gray-200 border-2 border-dashed rounded-xl w-16 h-16";
removeImageBtn.classList.add("hidden");
});
statusFilter.addEventListener("change", renderTasks);
assigneeFilter.addEventListener("change", renderTasks);
}
// Handle file upload
function handleFileChange(e) {
const file = e.target.files?.[0];
if (!file) {
removeImageBtn.click();
return;
}
if (!file.type.startsWith("image/")) {
alert("Please select an image file");
return;
}
const reader = new FileReader();
reader.onload = (event) => {
imagePreview.innerHTML = `<img src="${event.target.result}" class="w-full h-full rounded-xl object-cover border" />`;
removeImageBtn.classList.remove("hidden");
};
reader.readAsDataURL(file);
}
// Reset form
function resetForm() {
taskForm.reset();
titleError.classList.add("hidden");
assigneeError.classList.add("hidden");
dateError.classList.add("hidden");
document.getElementById('taskLocation').value = '';
imagePreview.innerHTML = "";
imagePreview.className = "bg-gray-200 border-2 border-dashed rounded-xl w-16 h-16";
removeImageBtn.classList.add("hidden");
}
// Add new task
function handleAddTask(e) {
e.preventDefault();
// Validate form
let isValid = true;
if (!taskTitle.value.trim()) {
titleError.textContent = "Task is required";
titleError.classList.remove("hidden");
isValid = false;
} else {
titleError.classList.add("hidden");
}
if (!assignedTo.value.trim()) {
assigneeError.textContent = "Assigned person is required";
assigneeError.classList.remove("hidden");
isValid = false;
} else {
assigneeError.classList.add("hidden");
}
if (!dateReported.value) {
dateError.textContent = "Date reported is required";
dateError.classList.remove("hidden");
isValid = false;
} else {
dateError.classList.add("hidden");
}
if (!isValid) return;
// Create new task
const newTask = {
id: "t_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
title: taskTitle.value.trim(),
location: document.getElementById('taskLocation').value.trim(),
assignedTo: assignedTo.value.trim(),
dateReported: dateReported.value,
status: "Pending",
imageDataUrl: imagePreview.querySelector("img")?.src || undefined,
completedImageDataUrl: undefined,
comments: [],
createdAt: new Date().toISOString()
};
tasks.unshift(newTask);
saveTasks();
resetForm();
updateAssigneeFilter();
renderTasks();
}
// Update task status
function updateTaskStatus(id, status) {
const task = tasks.find(t => t.id === id);
if (task) {
task.status = status;
saveTasks();
renderTasks();
}
}
// Add comment to task
function addComment(id, commentInput) {
const text = commentInput.value.trim();
const commentImageInput = document.getElementById(`commentImage-${id}`);
const file = commentImageInput.files?.[0];
if (!text && !file) return;
const task = tasks.find(t => t.id === id);
if (task) {
const reader = new FileReader();
if (file) {
reader.onload = (event) => {
const newComment = {
id: "c_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
text: text || "Added photo update",
imageDataUrl: event.target.result,
createdAt: new Date().toISOString()
};
task.comments.unshift(newComment);
saveTasks();
renderTasks();
commentInput.value = "";
commentImageInput.value = "";
};
reader.readAsDataURL(file);
} else {
const newComment = {
id: "c_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
text,
createdAt: new Date().toISOString()
};
task.comments.unshift(newComment);
saveTasks();
renderTasks();
commentInput.value = "";
}
}
}
// Toggle comments visibility
function toggleComments(id) {
const commentsDiv = document.getElementById(`comments-${id}`);
const toggleBtn = document.getElementById(`toggleComments-${id}`);
if (commentsDiv.classList.contains("hidden")) {
commentsDiv.classList.remove("hidden");
toggleBtn.textContent = "Hide updates";
} else {
commentsDiv.classList.add("hidden");
toggleBtn.textContent = `Show updates (${tasks.find(t => t.id === id).comments.length})`;
}
}
// Save tasks to localStorage
function saveTasks() {
localStorage.setItem("crm_tasks_v1", JSON.stringify(tasks));
}
// Update assignee filter options
function updateAssigneeFilter() {
const assignees = [...new Set(tasks.map(t => t.assignedTo.trim()))].filter(Boolean);
const assigneeSelect = document.getElementById("assigneeFilter");
// Keep current selection
const currentValue = assigneeSelect.value;
// Clear and rebuild options
assigneeSelect.innerHTML = '<option value="All">All</option>';
assignees.forEach(assignee => {
const option = document.createElement("option");
option.value = assignee;
option.textContent = assignee;
assigneeSelect.appendChild(option);
});
// Restore selection if still valid
if (assignees.includes(currentValue)) {
assigneeSelect.value = currentValue;
}
}
// Render tasks based on filters
function renderTasks() {
const statusFilterValue = statusFilter.value;
const assigneeFilterValue = assigneeFilter.value;
const locationFilterValue = document.getElementById('locationFilter').value;
const locationOrder = [
"Berthaphil I",
"Berthaphil II South",
"Berthaphil II North",
"Berthaphil Office Park",
"Clark Center",
"Berthaphil IV",
"Berthaphil VI",
"Berthaphil VIII"
];
const filteredTasks = tasks
.filter(task => {
const statusMatch = statusFilterValue === "All" || task.status === statusFilterValue;
const assigneeMatch = assigneeFilterValue === "All" || task.assignedTo.trim() === assigneeFilterValue;
const locationMatch = locationFilterValue === "All" || task.location === locationFilterValue;
return statusMatch && assigneeMatch && locationMatch;
})
.sort((a, b) => {
const aIndex = locationOrder.indexOf(a.location);
const bIndex = locationOrder.indexOf(b.location);
// If both locations are in our ordered list, sort by their position
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
// If only one location is in our ordered list, it comes first
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
// If neither location is in our ordered list, sort alphabetically
return (a.location || '').localeCompare(b.location || '');
});
if (filteredTasks.length === 0) {
emptyState.classList.remove("hidden");
tasksContainer.classList.add("hidden");
return;
}
emptyState.classList.add("hidden");
tasksContainer.classList.remove("hidden");
tasksContainer.innerHTML = filteredTasks.map(task => `
<div class="bg-white border rounded-2xl p-4 shadow-sm task-card">
<div class="flex items-start justify-between">
<h3 class="text-base font-semibold text-gray-900 cursor-pointer hover:underline" onclick="document.getElementById('taskDetails-${task.id}').classList.toggle('hidden')">${task.title}</h3>
<div class="flex items-center gap-2">
<span class="ml-2 inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${statusBadgeClasses(task.status)}">
${task.status}
</span>
<button data-task-id="${task.id}" class="delete-task-btn text-red-500 hover:text-red-700" title="Delete task" onclick="deleteTask('${task.id}')">
<i data-feather="trash-2" class="w-4 h-4"></i>
</button>
${task.status === 'Completed' ?
`<button data-task-id="${task.id}" class="text-green-600 hover:text-green-800" title="Mark as incomplete" onclick="updateTaskStatus('${task.id}', 'Pending')">
<i data-feather="rotate-ccw" class="w-4 h-4"></i>
</button>` : ''
}
</div>
</div>
</div>
<div id="taskDetails-${task.id}" class="hidden mt-2 p-3 bg-gray-50 rounded-lg">
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="font-medium">Assigned To:</span> ${task.assignedTo}
</div>
<div>
<span class="font-medium">Location:</span> ${task.location || 'Not specified'}
</div>
<div>
<span class="font-medium">Status:</span> ${task.status}
</div>
<div>
<span class="font-medium">Date Reported:</span> ${formatDate(task.dateReported)}
</div>
<div class="col-span-2">
<span class="font-medium">Created:</span> ${new Date(task.createdAt).toLocaleString()}
</div>
</div>
<div class="mt-4 flex items-center gap-3">
<div class="flex flex-col gap-1">
${task.imageDataUrl ?
`<img src="${task.imageDataUrl}" alt="Attachment" class="w-16 h-16 rounded-xl object-cover border cursor-pointer" onclick="showImageModal('${task.imageDataUrl}')" />` :
`<label class="flex items-center justify-center w-16 h-16 rounded-xl border border-gray-300 bg-gray-50 hover:bg-gray-100 cursor-pointer text-xs font-medium text-gray-700">
<input type="file" id="initialImage-${task.id}" accept="image/*" class="hidden" onchange="handleInitialImageUpload('${task.id}', this)">
Upload
</label>`
}
<span class="text-xs text-gray-500 text-center">Before Task</span>
</div>
${task.completedImageDataUrl ?
`<img src="${task.completedImageDataUrl}" alt="Completed Photo" class="w-16 h-16 rounded-xl object-cover border cursor-pointer" onclick="showImageModal('${task.completedImageDataUrl}')" />` :
task.status === 'Completed' ? `
<div class="flex flex-col gap-1">
<label class="flex items-center justify-center w-16 h-16 rounded-xl border border-gray-300 bg-gray-50 hover:bg-gray-100 cursor-pointer text-xs font-medium text-gray-700">
<input type="file" id="completedImage-${task.id}" accept="image/*" class="hidden" onchange="handleCompletedImageUpload('${task.id}', this)">
Upload
</label>
<span class="text-xs text-gray-500 text-center">Completed</span>
</div>
` : ''
}
<div class="flex-1">
<label class="block text-sm text-gray-700 mb-1">Progress</label>
<select
onchange="updateTaskStatus('${task.id}', this.value)"
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
${STATUS_OPTIONS.map(s => `
<option value="${s}" ${task.status === s ? 'selected' : ''}>${s}</option>
`).join('')}
</select>
</div>
</div>
<div class="mt-4">
<button
type="button"
id="toggleComments-${task.id}"
onclick="toggleComments('${task.id}')"
class="text-sm text-blue-700 hover:text-blue-800 underline"
>
Show updates (${task.comments.length})
</button>
<div id="comments-${task.id}" class="mt-3 hidden">
<div class="space-y-2 comment-box">
${task.comments.length === 0 ?
`<div class="text-sm text-gray-600">No updates yet.</div>` :
task.comments.map(c => `
<div class="border rounded-lg p-2">
<div class="text-xs text-gray-500">${new Date(c.createdAt).toLocaleString()}</div>
<div class="text-sm text-gray-800 mt-1">${c.text}</div>
${c.imageDataUrl ? `
<div class="mt-2">
<img src="${c.imageDataUrl}" alt="Update photo" class="w-full max-h-40 object-contain rounded cursor-pointer" onclick="showImageModal('${c.imageDataUrl}')" />
<div class="text-xs text-gray-500 mt-1">Photo After Completion</div>
</div>
` : ''}
</div>
`).join('')
}
</div>
</div>
</div>
<div class="mt-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Add update</label>
<div class="flex flex-col gap-2">
<div class="flex items-start gap-2">
<textarea
id="commentInput-${task.id}"
placeholder="Write a quick update..."
rows="2"
class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
></textarea>
<button
onclick="addComment('${task.id}', document.getElementById('commentInput-${task.id}'))"
class="self-stretch px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm"
>
Post
</button>
</div>
<div class="flex items-center gap-2">
<label class="inline-flex items-center justify-center px-3 py-1 border border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 cursor-pointer text-xs font-medium text-gray-700">
<input type="file" id="commentImage-${task.id}" accept="image/*" class="hidden">
Add Photo
</label>
<span class="text-xs text-gray-500">Photo After Completion</span>
</div>
</div>
</div>
</div>
<div class="mt-4">
</div>
`).join('');
}
// Encode tasks for URL sharing
function encodeTasksForURL() {
const compressed = LZString.compressToEncodedURIComponent(JSON.stringify(tasks));
const url = new URL(window.location.href);
url.searchParams.set('tasks', compressed);
window.history.replaceState({}, '', url);
return url.toString();
}
// Decode tasks from URL
function decodeTasksFromURL() {
const urlParams = new URLSearchParams(window.location.search);
const compressed = urlParams.get('tasks');
if (compressed) {
try {
const decompressed = LZString.decompressFromEncodedURIComponent(compressed);
return JSON.parse(decompressed);
} catch (e) {
console.error('Error parsing URL tasks:', e);
}
}
return null;
}
// Share button functionality
function setupShareButton() {
const shareBtn = document.createElement('button');
shareBtn.className = 'fixed bottom-6 right-6 bg-blue-600 text-white p-3 rounded-full shadow-lg hover:bg-blue-700 transition-colors';
shareBtn.innerHTML = '<i data-feather="share-2"></i>';
shareBtn.title = 'Share current tasks';
shareBtn.onclick = () => {
const shareUrl = encodeTasksForURL();
navigator.clipboard.writeText(shareUrl).then(() => {
alert('Shareable link copied to clipboard!');
}).catch(() => {
prompt('Copy this link to share:', shareUrl);
});
};
document.body.appendChild(shareBtn);
feather.replace();
}
// Delete task function
function deleteTask(taskId) {
if (confirm('Are you sure you want to delete this task?')) {
tasks = tasks.filter(task => task.id !== taskId);
saveTasks();
updateAssigneeFilter();
renderTasks();
}
return false; // Prevent event bubbling
}
// Initialize the app
document.addEventListener("DOMContentLoaded", () => {
// Add LZString compression library
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/lz-string@1.4.4/libs/lz-string.min.js';
script.onload = () => {
// Try to load from URL first
const urlTasks = decodeTasksFromURL();
if (urlTasks) {
tasks = urlTasks;
saveTasks();
}
init();
updateAssigneeFilter();
setupShareButton();
// Initialize location filter
document.getElementById('locationFilter').addEventListener('change', renderTasks);
};
document.head.appendChild(script);
});
// Close modal with ESC key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.getElementById('imageModal').classList.add('hidden');
}
});
</script>
</body>
</html>