feat: implement project status management with 'on_hold' state and associated UI updates

-feat: ability to hard delete projects, plus a soft delete with auto pruning.
This commit is contained in:
serversdwn
2026-02-19 15:23:02 +00:00
parent dc77a362ce
commit 65362bab21
9 changed files with 300 additions and 20 deletions

View File

@@ -13,6 +13,8 @@
</div>
{% if project.status == 'active' %}
<span class="px-3 py-1 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">Active</span>
{% elif project.status == 'on_hold' %}
<span class="px-3 py-1 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400 rounded-full">On Hold</span>
{% elif project.status == 'completed' %}
<span class="px-3 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">Completed</span>
{% elif project.status == 'archived' %}

View File

@@ -34,6 +34,10 @@
<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 rounded-full">
Active
</span>
{% elif item.project.status == 'on_hold' %}
<span class="px-2 py-1 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400 rounded-full">
On Hold
</span>
{% elif item.project.status == 'completed' %}
<span class="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 rounded-full">
Completed

View File

@@ -16,6 +16,8 @@
{% if item.project.status == 'active' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 rounded-full">Active</span>
{% elif item.project.status == 'on_hold' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400 rounded-full">On Hold</span>
{% elif item.project.status == 'completed' %}
<span class="shrink-0 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">Completed</span>
{% elif item.project.status == 'archived' %}

View File

@@ -27,6 +27,20 @@
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">On Hold</p>
<p class="text-3xl font-bold text-amber-600 dark:text-amber-400">{{ on_hold_projects }}</p>
</div>
<div class="p-3 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
<svg class="w-8 h-8 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<div class="flex items-center justify-between">
<div>

View File

@@ -279,6 +279,7 @@
<select name="status" id="settings-status"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
<option value="active">Active</option>
<option value="on_hold">On Hold</option>
<option value="completed">Completed</option>
<option value="archived">Archived</option>
</select>
@@ -329,14 +330,39 @@
<!-- Danger Zone -->
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-red-600 dark:text-red-400 mb-4">Danger Zone</h3>
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">
Archive this project to remove it from active listings. All data will be preserved.
</p>
<button onclick="archiveProject()"
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
Archive Project
</button>
<div class="space-y-3">
<!-- On Hold -->
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 flex items-center justify-between gap-4">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Put Project On Hold</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Pause this project without archiving. Assignments and schedules remain in place.</p>
</div>
<div id="hold-btn-container" class="shrink-0">
<!-- Rendered by updateDangerZone() based on current status -->
</div>
</div>
<!-- Archive -->
<div class="bg-gray-50 dark:bg-gray-700/40 border border-gray-200 dark:border-gray-600 rounded-lg p-4 flex items-center justify-between gap-4">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Archive Project</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Remove from active listings. All data is preserved and can be restored.</p>
</div>
<button onclick="archiveProject()"
class="shrink-0 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-sm">
Archive
</button>
</div>
<!-- Delete -->
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex items-center justify-between gap-4">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">Delete Project</p>
<p class="text-sm text-gray-600 dark:text-gray-400">Permanently removes all project data after a 60-day grace period. This action is difficult to undo.</p>
</div>
<button onclick="openDeleteModal()"
class="shrink-0 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm">
Delete
</button>
</div>
</div>
</div>
</div>
@@ -596,6 +622,40 @@
</div>
</div>
<!-- Delete Project Confirmation Modal -->
<div id="delete-project-modal" class="hidden fixed inset-0 bg-black bg-opacity-60 z-50 flex items-center justify-center">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-md m-4 p-6">
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-red-100 dark:bg-red-900/40 rounded-lg">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"></path>
</svg>
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Delete Project</h3>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
This project will be soft-deleted and <strong class="text-gray-900 dark:text-white">permanently removed after 60 days</strong>. All associated locations, assignments, and sessions will be lost.
</p>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-4">
Type <span class="font-mono font-bold text-red-600 dark:text-red-400">delete</span> to confirm:
</p>
<input type="text" id="delete-confirm-input"
placeholder="type delete"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4 focus:outline-none focus:ring-2 focus:ring-red-500"
autocomplete="off">
<div class="flex gap-3 justify-end">
<button onclick="closeDeleteModal()"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm">
Cancel
</button>
<button id="confirm-delete-btn" disabled onclick="executeDeleteProject()"
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm disabled:opacity-40 disabled:cursor-not-allowed">
Delete Project
</button>
</div>
</div>
</div>
<script>
const projectId = "{{ project_id }}";
let editingLocationId = null;
@@ -662,6 +722,7 @@ async function loadProjectDetails() {
}
document.getElementById('settings-error').classList.add('hidden');
updateDangerZone();
} catch (err) {
console.error('Failed to load project details:', err);
}
@@ -1027,6 +1088,78 @@ function archiveProject() {
document.getElementById('project-settings-form').dispatchEvent(new Event('submit'));
}
async function holdProject() {
try {
const response = await fetch(`/api/projects/${projectId}/hold`, { method: 'POST' });
if (!response.ok) throw new Error('Failed to put project on hold');
await loadProjectDetails();
updateDangerZone();
htmx.trigger('#project-header', 'load');
} catch (err) {
alert('Failed to put project on hold: ' + err.message);
}
}
async function unholdProject() {
try {
const response = await fetch(`/api/projects/${projectId}/unhold`, { method: 'POST' });
if (!response.ok) throw new Error('Failed to resume project');
await loadProjectDetails();
updateDangerZone();
htmx.trigger('#project-header', 'load');
} catch (err) {
alert('Failed to resume project: ' + err.message);
}
}
function updateDangerZone() {
const status = document.getElementById('settings-status').value;
const container = document.getElementById('hold-btn-container');
if (!container) return;
if (status === 'on_hold') {
container.innerHTML = `<button onclick="unholdProject()"
class="px-4 py-2 bg-amber-500 text-white rounded-lg hover:bg-amber-600 transition-colors text-sm">
Resume Project
</button>`;
} else {
container.innerHTML = `<button onclick="holdProject()"
class="px-4 py-2 border border-amber-500 text-amber-600 dark:text-amber-400 rounded-lg hover:bg-amber-50 dark:hover:bg-amber-900/20 transition-colors text-sm">
Put On Hold
</button>`;
}
}
function openDeleteModal() {
document.getElementById('delete-confirm-input').value = '';
document.getElementById('confirm-delete-btn').disabled = true;
document.getElementById('delete-project-modal').classList.remove('hidden');
document.getElementById('delete-confirm-input').focus();
}
function closeDeleteModal() {
document.getElementById('delete-project-modal').classList.add('hidden');
}
async function executeDeleteProject() {
try {
const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete project');
closeDeleteModal();
window.location.href = '/projects';
} catch (err) {
alert('Failed to delete project: ' + err.message);
}
}
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('delete-confirm-input');
if (input) {
input.addEventListener('input', function() {
document.getElementById('confirm-delete-btn').disabled = this.value !== 'delete';
});
}
});
// ============================================================================
// Schedule Modal Functions
// ============================================================================

View File

@@ -18,7 +18,7 @@
</div>
<!-- Summary Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8"
hx-get="/api/projects/stats"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
@@ -27,6 +27,7 @@
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-24 rounded-xl"></div>
</div>
<!-- Tabs -->
@@ -43,6 +44,11 @@
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">
Active
</button>
<button onclick="switchTab('on_hold')"
id="tab-on_hold"
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">
On Hold
</button>
<button onclick="switchTab('completed')"
id="tab-completed"
class="tab-button border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 px-1 py-4 text-sm font-medium">