feat: add functionality to manage deleted projects in settings

- Introduced a new section for displaying soft-deleted projects.
- Implemented loading of deleted projects via an API call.
- Added restore and permanently delete options for each deleted project.
- Integrated loading of deleted projects when the data tab is shown.
This commit is contained in:
2026-04-01 05:42:10 +00:00
parent 20e180644e
commit f50cf2b7f6
4 changed files with 458 additions and 286 deletions

View File

@@ -355,6 +355,25 @@
</form>
<div id="uploadResult" class="hidden mt-3"></div>
</div>
<!-- Deleted Projects -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Deleted Projects</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Projects that have been soft-deleted. Restore them or permanently remove them.</p>
</div>
<button onclick="loadDeletedProjects()" class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Refresh
</button>
</div>
<div id="deletedProjectsList">
<p class="text-sm text-gray-400 dark:text-gray-500 italic">Loading...</p>
</div>
</div>
</div>
</div>
@@ -584,6 +603,67 @@
</style>
<script>
// ========== DELETED PROJECTS ==========
async function loadDeletedProjects() {
const container = document.getElementById('deletedProjectsList');
container.innerHTML = '<p class="text-sm text-gray-400 dark:text-gray-500 italic">Loading...</p>';
try {
const resp = await fetch('/api/projects/deleted');
const projects = await resp.json();
if (!projects.length) {
container.innerHTML = '<p class="text-sm text-gray-400 dark:text-gray-500 italic">No deleted projects.</p>';
return;
}
container.innerHTML = `
<div class="divide-y divide-gray-100 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
${projects.map(p => `
<div class="flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-gray-900/30">
<div>
<div class="font-medium text-gray-900 dark:text-white">${p.name}</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
${p.client_name ? p.client_name + ' &middot; ' : ''}Deleted ${new Date(p.deleted_at).toLocaleDateString()}
</div>
</div>
<div class="flex items-center gap-2 ml-4">
<button onclick="restoreProject('${p.id}', '${p.name.replace(/'/g, "\\'")}')"
class="px-3 py-1 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
Restore
</button>
<button onclick="permanentlyDeleteProject('${p.id}', '${p.name.replace(/'/g, "\\'")}')"
class="px-3 py-1 text-xs bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
Delete Permanently
</button>
</div>
</div>`).join('')}
</div>`;
} catch (e) {
container.innerHTML = '<p class="text-sm text-red-500">Failed to load deleted projects.</p>';
}
}
async function restoreProject(projectId, name) {
if (!confirm(`Restore "${name}"?`)) return;
const resp = await fetch(`/api/projects/${projectId}/restore`, { method: 'POST' });
if (resp.ok) {
loadDeletedProjects();
} else {
const d = await resp.json();
alert('Failed to restore: ' + (d.detail || 'Unknown error'));
}
}
async function permanentlyDeleteProject(projectId, name) {
if (!confirm(`Permanently delete "${name}" and all its data? This cannot be undone.`)) return;
const resp = await fetch(`/api/projects/${projectId}/permanent`, { method: 'DELETE' });
if (resp.ok) {
loadDeletedProjects();
} else {
const d = await resp.json();
alert('Failed to delete: ' + (d.detail || 'Unknown error'));
}
}
// ========== TAB MANAGEMENT ==========
function showTab(tabName) {
@@ -609,9 +689,10 @@ function showTab(tabName) {
// Save last active tab to localStorage
localStorage.setItem('settings-last-tab', tabName);
// Load roster table when data tab is shown
// Load roster table and deleted projects when data tab is shown
if (tabName === 'data') {
loadRosterTable();
loadDeletedProjects();
}
}