This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

View File

@@ -254,6 +254,83 @@
</div>
</div>
</div>
<!-- Closure Checklist -->
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-6 md:col-span-2" id="closureChecklistCard" style="display: none;">
<div class="flex items-center justify-between pb-2 mb-3 border-b border-neutral-200 dark:border-neutral-700">
<h6 class="mb-0 font-semibold">Closure Checklist</h6>
<div class="flex items-center gap-2">
<input type="text" id="newChecklistName" class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" placeholder="Add checklist item...">
<label class="flex items-center gap-1 text-sm"><input type="checkbox" id="newChecklistRequired" class="mr-1"> Required</label>
<button type="button" class="px-3 py-2 bg-success-600 text-white rounded-lg hover:bg-success-700" id="addChecklistBtn">
<i class="fa-solid fa-plus mr-1"></i> Add
</button>
</div>
</div>
<ul id="checklistItems" class="space-y-2"></ul>
</div>
<!-- File Alerts -->
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-6 md:col-span-2" id="fileAlertsCard" style="display: none;">
<div class="pb-2 mb-3 border-b border-neutral-200 dark:border-neutral-700">
<h6 class="mb-0 font-semibold">File Alerts</h6>
</div>
<div class="grid grid-cols-1 md:grid-cols-5 gap-2 mb-3">
<div>
<input type="text" id="alertType" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" placeholder="Type (e.g., follow_up)">
</div>
<div class="md:col-span-2">
<input type="text" id="alertTitle" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" placeholder="Title">
</div>
<div>
<input type="date" id="alertDate" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg">
</div>
<div class="flex items-center gap-2">
<label class="flex items-center gap-1 text-sm"><input type="checkbox" id="alertNotifyAttorney" checked> Attorney</label>
<label class="flex items-center gap-1 text-sm"><input type="checkbox" id="alertNotifyAdmin"> Admin</label>
</div>
<div class="md:col-span-4">
<input type="text" id="alertMessage" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" placeholder="Message">
</div>
<div>
<button type="button" class="w-full px-3 py-2 bg-success-600 text-white rounded-lg hover:bg-success-700" id="createAlertBtn">
<i class="fa-solid fa-bell mr-1"></i> Create
</button>
</div>
</div>
<ul id="alertsList" class="space-y-2"></ul>
</div>
<!-- File Relationships -->
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-6 md:col-span-2" id="fileRelationshipsCard" style="display: none;">
<div class="pb-2 mb-3 border-b border-neutral-200 dark:border-neutral-700">
<h6 class="mb-0 font-semibold">File Relationships</h6>
</div>
<div class="grid grid-cols-1 md:grid-cols-5 gap-2 mb-3">
<div>
<input type="text" id="relTargetFileNo" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" placeholder="Target File #">
</div>
<div>
<select id="relType" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg">
<option value="related">related</option>
<option value="parent">parent</option>
<option value="child">child</option>
<option value="duplicate">duplicate</option>
<option value="conflict">conflict</option>
<option value="referral">referral</option>
</select>
</div>
<div class="md:col-span-3">
<input type="text" id="relNotes" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" placeholder="Notes (optional)">
</div>
<div class="md:col-span-5">
<button type="button" class="px-3 py-2 bg-success-600 text-white rounded-lg hover:bg-success-700" id="addRelationshipBtn">
<i class="fa-solid fa-link mr-1"></i> Link Files
</button>
</div>
</div>
<ul id="relationshipsList" class="space-y-2"></ul>
</div>
</div>
</form>
</div>
@@ -430,6 +507,12 @@ function setupEventListeners() {
document.getElementById('deleteFileBtn').addEventListener('click', deleteFile);
document.getElementById('closeFileBtn').addEventListener('click', closeFile);
document.getElementById('reopenFileBtn').addEventListener('click', reopenFile);
// Checklist
document.getElementById('addChecklistBtn').addEventListener('click', addChecklistItem);
// Alerts
document.getElementById('createAlertBtn').addEventListener('click', createAlert);
// Relationships
document.getElementById('addRelationshipBtn').addEventListener('click', addRelationship);
// Other buttons
document.getElementById('statsBtn').addEventListener('click', showStats);
@@ -676,6 +759,9 @@ async function editFile(fileNo) {
document.getElementById('fileActions').style.display = 'block';
document.getElementById('financialSummaryCard').style.display = 'block';
document.getElementById('documentsCard').style.display = 'block'; // Show documents card for editing
document.getElementById('closureChecklistCard').style.display = 'block';
document.getElementById('fileAlertsCard').style.display = 'block';
document.getElementById('fileRelationshipsCard').style.display = 'block';
document.getElementById('fileNo').readOnly = true;
// Show/hide close/reopen buttons based on status
@@ -686,6 +772,9 @@ async function editFile(fileNo) {
// Load financial summary
loadFinancialSummary(fileNo);
loadDocuments(fileNo); // Load documents for editing
loadClosureChecklist(fileNo);
loadAlerts(fileNo);
loadRelationships(fileNo);
openModal('fileModal');
@@ -1214,5 +1303,342 @@ async function updateDocumentDescription(docId, description) {
showAlert('Error updating description: ' + error.message, 'danger');
}
}
// Closure Checklist
async function loadClosureChecklist(fileNo) {
try {
const res = await window.http.wrappedFetch(`/api/file-management/${encodeURIComponent(fileNo)}/closure-checklist`);
if (!res.ok) throw await window.http.toError(res, 'Failed to load checklist');
const items = await res.json();
const list = document.getElementById('checklistItems');
list.innerHTML = '';
if (!items || items.length === 0) {
list.innerHTML = '<li class="text-neutral-500 text-sm">No checklist items yet.</li>';
return;
}
items.forEach(item => {
const li = document.createElement('li');
li.dataset.itemId = item.id;
li.className = 'flex items-center justify-between gap-2 border border-neutral-200 dark:border-neutral-700 rounded-md px-3 py-2';
li.innerHTML = `
<div class="flex items-center gap-3">
<input type="checkbox" ${item.is_completed ? 'checked' : ''} onchange="toggleChecklistItem(${item.id}, this.checked)" />
<div>
<div class="font-medium">${_escapeHtml(item.item_name)}</div>
<div class="text-xs text-neutral-500">${_escapeHtml(item.item_description || '')}</div>
</div>
${item.is_required ? '<span class="text-xs px-2 py-0.5 rounded bg-red-100 text-red-700">Required</span>' : ''}
</div>
<div class="flex items-center gap-2">
<button class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded" onclick="editChecklistItem(${item.id})">Edit</button>
<button class="px-2 py-1 text-xs border border-red-600 text-red-700 rounded" onclick="deleteChecklistItem(${item.id})">Delete</button>
</div>
`;
list.appendChild(li);
});
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error loading checklist'), 'danger');
}
}
async function addChecklistItem() {
const name = (document.getElementById('newChecklistName').value || '').trim();
const isRequired = !!document.getElementById('newChecklistRequired').checked;
if (!editingFileNo) return;
if (!name) {
showAlert('Enter a checklist item name', 'warning');
return;
}
// optimistic add
const tempId = 'temp-' + Date.now();
const list = document.getElementById('checklistItems');
const li = document.createElement('li');
li.dataset.itemId = tempId;
li.className = 'flex items-center justify-between gap-2 border border-neutral-200 dark:border-neutral-700 rounded-md px-3 py-2 opacity-60';
li.innerHTML = `
<div class="flex items-center gap-3">
<input type="checkbox" />
<div>
<div class="font-medium">${_escapeHtml(name)}</div>
</div>
${isRequired ? '<span class="text-xs px-2 py-0.5 rounded bg-red-100 text-red-700">Required</span>' : ''}
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-neutral-500">Saving...</span>
</div>
`;
list.appendChild(li);
try {
const res = await window.http.wrappedFetch(`/api/file-management/${encodeURIComponent(editingFileNo)}/closure-checklist`, {
method: 'POST',
body: JSON.stringify({ item_name: name, is_required: isRequired })
});
if (!res.ok) throw await window.http.toError(res, 'Failed to add item');
const saved = await res.json();
document.getElementById('newChecklistName').value = '';
document.getElementById('newChecklistRequired').checked = true;
// Refresh list for clean state
loadClosureChecklist(editingFileNo);
} catch (err) {
li.remove();
showAlert(window.http.formatAlert(err, 'Error adding item'), 'danger');
}
}
async function toggleChecklistItem(itemId, checked) {
try {
const res = await window.http.wrappedFetch(`/api/file-management/closure-checklist/${itemId}`, {
method: 'PUT',
body: JSON.stringify({ is_completed: !!checked })
});
if (!res.ok) throw await window.http.toError(res, 'Failed to update item');
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error updating item'), 'danger');
loadClosureChecklist(editingFileNo);
}
}
function editChecklistItem(itemId) {
const newName = prompt('Update item name (leave blank to skip):');
if (newName === null) return;
const newNotes = prompt('Notes (optional, leave blank to skip):');
updateChecklistItem(itemId, newName, newNotes || undefined);
}
async function updateChecklistItem(itemId, newName, notes) {
const payload = {};
if (newName && newName.trim()) payload.item_name = newName.trim();
if (notes !== undefined) payload.notes = notes;
try {
const res = await window.http.wrappedFetch(`/api/file-management/closure-checklist/${itemId}`, {
method: 'PUT',
body: JSON.stringify(payload)
});
if (!res.ok) throw await window.http.toError(res, 'Failed to update item');
loadClosureChecklist(editingFileNo);
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error updating item'), 'danger');
}
}
async function deleteChecklistItem(itemId) {
if (!confirm('Delete this checklist item?')) return;
const li = document.querySelector(`li[data-item-id="${itemId}"]`);
if (li) li.remove();
try {
const res = await window.http.wrappedFetch(`/api/file-management/closure-checklist/${itemId}`, { method: 'DELETE' });
if (!res.ok) throw await window.http.toError(res, 'Failed to delete item');
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error deleting item'), 'danger');
loadClosureChecklist(editingFileNo);
}
}
// Alerts
async function loadAlerts(fileNo) {
try {
const res = await window.http.wrappedFetch(`/api/file-management/${encodeURIComponent(fileNo)}/alerts?active_only=true&upcoming_only=false&limit=100`);
if (!res.ok) throw await window.http.toError(res, 'Failed to load alerts');
const alerts = await res.json();
const list = document.getElementById('alertsList');
list.innerHTML = '';
if (!alerts || alerts.length === 0) {
list.innerHTML = '<li class="text-neutral-500 text-sm">No alerts yet.</li>';
return;
}
alerts.forEach(a => {
const li = document.createElement('li');
li.dataset.alertId = a.id;
li.className = 'flex items-center justify-between gap-2 border border-neutral-200 dark:border-neutral-700 rounded-md px-3 py-2';
li.innerHTML = `
<div>
<div class="font-medium">${_escapeHtml(a.title)} <span class="text-xs text-neutral-500">(${_escapeHtml(a.alert_type)})</span></div>
<div class="text-xs text-neutral-500">${formatDate(a.alert_date)} • ${_escapeHtml(a.message || '')}</div>
</div>
<div class="flex items-center gap-2">
${a.is_acknowledged ? '<span class="text-xs text-green-700">Acknowledged</span>' : `<button class="px-2 py-1 text-xs border border-success-600 text-success-700 rounded" onclick="ackAlert(${a.id})">Acknowledge</button>`}
<button class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded" onclick="editAlert(${a.id})">Edit</button>
<button class="px-2 py-1 text-xs border border-red-600 text-red-700 rounded" onclick="deleteAlert(${a.id})">Delete</button>
</div>
`;
list.appendChild(li);
});
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error loading alerts'), 'danger');
}
}
async function createAlert() {
if (!editingFileNo) return;
const alert_type = (document.getElementById('alertType').value || '').trim();
const title = (document.getElementById('alertTitle').value || '').trim();
const message = (document.getElementById('alertMessage').value || '').trim();
const alert_date = document.getElementById('alertDate').value;
const notify_attorney = !!document.getElementById('alertNotifyAttorney').checked;
const notify_admin = !!document.getElementById('alertNotifyAdmin').checked;
if (!alert_type || !title || !alert_date) {
showAlert('Type, title, and date are required', 'warning');
return;
}
// optimistic row
const tempId = 'temp-' + Date.now();
const list = document.getElementById('alertsList');
const li = document.createElement('li');
li.dataset.alertId = tempId;
li.className = 'flex items-center justify-between gap-2 border border-neutral-200 dark:border-neutral-700 rounded-md px-3 py-2 opacity-60';
li.innerHTML = `
<div>
<div class="font-medium">${_escapeHtml(title)} <span class="text-xs text-neutral-500">(${_escapeHtml(alert_type)})</span></div>
<div class="text-xs text-neutral-500">${_escapeHtml(alert_date)} • ${_escapeHtml(message)}</div>
</div>
<div class="text-xs text-neutral-500">Saving...</div>
`;
list.appendChild(li);
try {
const res = await window.http.wrappedFetch(`/api/file-management/${encodeURIComponent(editingFileNo)}/alerts`, {
method: 'POST',
body: JSON.stringify({ alert_type, title, message, alert_date, notify_attorney, notify_admin })
});
if (!res.ok) throw await window.http.toError(res, 'Failed to create alert');
document.getElementById('alertType').value = '';
document.getElementById('alertTitle').value = '';
document.getElementById('alertMessage').value = '';
document.getElementById('alertDate').value = '';
document.getElementById('alertNotifyAttorney').checked = true;
document.getElementById('alertNotifyAdmin').checked = false;
loadAlerts(editingFileNo);
} catch (err) {
li.remove();
showAlert(window.http.formatAlert(err, 'Error creating alert'), 'danger');
}
}
async function ackAlert(alertId) {
try {
const res = await window.http.wrappedFetch(`/api/file-management/alerts/${alertId}/acknowledge`, { method: 'POST' });
if (!res.ok) throw await window.http.toError(res, 'Failed to acknowledge alert');
loadAlerts(editingFileNo);
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error acknowledging alert'), 'danger');
}
}
function editAlert(alertId) {
const newTitle = prompt('New title (leave blank to skip):');
if (newTitle === null) return;
const newMessage = prompt('New message (leave blank to skip):');
updateAlert(alertId, newTitle, newMessage);
}
async function updateAlert(alertId, title, message) {
const payload = {};
if (title && title.trim()) payload.title = title.trim();
if (message && message.trim()) payload.message = message.trim();
try {
const res = await window.http.wrappedFetch(`/api/file-management/alerts/${alertId}`, {
method: 'PUT',
body: JSON.stringify(payload)
});
if (!res.ok) throw await window.http.toError(res, 'Failed to update alert');
loadAlerts(editingFileNo);
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error updating alert'), 'danger');
}
}
async function deleteAlert(alertId) {
if (!confirm('Delete this alert?')) return;
const li = document.querySelector(`li[data-alert-id="${alertId}"]`);
if (li) li.remove();
try {
const res = await window.http.wrappedFetch(`/api/file-management/alerts/${alertId}`, { method: 'DELETE' });
if (!res.ok) throw await window.http.toError(res, 'Failed to delete alert');
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error deleting alert'), 'danger');
loadAlerts(editingFileNo);
}
}
// Relationships
async function loadRelationships(fileNo) {
try {
const res = await window.http.wrappedFetch(`/api/file-management/${encodeURIComponent(fileNo)}/relationships`);
if (!res.ok) throw await window.http.toError(res, 'Failed to load relationships');
const rels = await res.json();
const list = document.getElementById('relationshipsList');
list.innerHTML = '';
if (!rels || rels.length === 0) {
list.innerHTML = '<li class="text-neutral-500 text-sm">No relationships yet.</li>';
return;
}
rels.forEach(r => {
const li = document.createElement('li');
li.dataset.relationshipId = r.id;
li.className = 'flex items-center justify-between gap-2 border border-neutral-200 dark:border-neutral-700 rounded-md px-3 py-2';
li.innerHTML = `
<div>
<div class="font-medium">${_escapeHtml(r.relationship_type)} → ${_escapeHtml(r.other_file_no)}</div>
<div class="text-xs text-neutral-500">${_escapeHtml(r.notes || '')}</div>
</div>
<div>
<button class="px-2 py-1 text-xs border border-red-600 text-red-700 rounded" onclick="deleteRelationship(${r.id})">Remove</button>
</div>
`;
list.appendChild(li);
});
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error loading relationships'), 'danger');
}
}
async function addRelationship() {
const target = (document.getElementById('relTargetFileNo').value || '').trim();
const relationship_type = document.getElementById('relType').value;
const notes = (document.getElementById('relNotes').value || '').trim();
if (!editingFileNo) return;
if (!target) { showAlert('Enter a target file #', 'warning'); return; }
// optimistic
const tempId = 'temp-' + Date.now();
const list = document.getElementById('relationshipsList');
const li = document.createElement('li');
li.dataset.relationshipId = tempId;
li.className = 'flex items-center justify-between gap-2 border border-neutral-200 dark:border-neutral-700 rounded-md px-3 py-2 opacity-60';
li.innerHTML = `
<div>
<div class="font-medium">${_escapeHtml(relationship_type)} → ${_escapeHtml(target)}</div>
<div class="text-xs text-neutral-500">${_escapeHtml(notes)}</div>
</div>
<div class="text-xs text-neutral-500">Saving...</div>
`;
list.appendChild(li);
try {
const res = await window.http.wrappedFetch(`/api/file-management/${encodeURIComponent(editingFileNo)}/relationships`, {
method: 'POST',
body: JSON.stringify({ target_file_no: target, relationship_type, notes })
});
if (!res.ok) throw await window.http.toError(res, 'Failed to link files');
document.getElementById('relTargetFileNo').value = '';
document.getElementById('relNotes').value = '';
loadRelationships(editingFileNo);
} catch (err) {
li.remove();
showAlert(window.http.formatAlert(err, 'Error linking files'), 'danger');
}
}
async function deleteRelationship(id) {
if (!confirm('Remove this relationship?')) return;
const li = document.querySelector(`li[data-relationship-id="${id}"]`);
if (li) li.remove();
try {
const res = await window.http.wrappedFetch(`/api/file-management/relationships/${id}`, { method: 'DELETE' });
if (!res.ok) throw await window.http.toError(res, 'Failed to remove relationship');
} catch (err) {
showAlert(window.http.formatAlert(err, 'Error removing relationship'), 'danger');
loadRelationships(editingFileNo);
}
}
</script>
{% endblock %}