Files
delphi-database/templates/documents.html
HotSwapp bac8cc4bd5 changes
2025-08-18 20:20:04 -05:00

1986 lines
98 KiB
HTML

{% extends "base.html" %}
{% block title %}Document Management - Delphi Database{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="space-y-6">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3">
<div class="flex items-center justify-center w-10 h-10 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl">
<i class="fa-regular fa-file-lines text-lg"></i>
</div>
<h1 class="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Document Management</h1>
</div>
<div class="flex items-center gap-3">
<button id="newTemplateBtn" class="flex items-center gap-2 px-4 py-2 bg-success-600 text-white hover:bg-success-700 rounded-lg transition-colors duration-200">
<i class="fa-solid fa-circle-plus"></i>
<span>New Template</span>
<kbd class="hidden sm:inline-block ml-2 px-1.5 py-0.5 bg-success-700 rounded text-xs">Ctrl+N</kbd>
</button>
<button id="generateDocBtn" class="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors duration-200">
<i class="fa-regular fa-file-lines"></i>
<span>Generate Document</span>
</button>
<button id="newQdroBtn" class="flex items-center gap-2 px-4 py-2 bg-warning-600 text-white hover:bg-warning-700 rounded-lg transition-colors duration-200">
<i class="fa-regular fa-file-lines"></i>
<span>New QDRO</span>
</button>
<button id="statsBtn" class="flex items-center gap-2 px-4 py-2 bg-info-600 text-white hover:bg-info-700 rounded-lg transition-colors duration-200">
<i class="fa-solid fa-chart-line"></i>
<span>Statistics</span>
</button>
</div>
</div>
<!-- Document Management Tabs -->
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden">
<nav class="flex border-b border-neutral-200 dark:border-neutral-700">
<button class="flex-1 py-4 px-6 text-center border-b-2 border-transparent hover:border-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition duration-300" onclick="openTab(event, 'templates')" id="templates-tab">
<i class="fa-regular fa-file-lines mr-2"></i> Templates
</button>
<button class="flex-1 py-4 px-6 text-center border-b-2 border-transparent hover:border-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition duration-300" onclick="openTab(event, 'qdros')" id="qdros-tab">
<i class="fa-regular fa-file-lines mr-2"></i> QDROs
</button>
<button class="flex-1 py-4 px-6 text-center border-b-2 border-transparent hover:border-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition duration-300" onclick="openTab(event, 'generated')" id="generated-tab">
<i class="fa-regular fa-file-pdf mr-2"></i> Generated Documents
</button>
</nav>
<div id="templates" class="tabcontent p-6">
<div class="mt-3 bg-white dark:bg-neutral-800 rounded-xl shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 items-center">
<div class="md:col-span-2">
<h5 class="mb-0 font-semibold flex items-center gap-2"><i class="fa-regular fa-file-lines"></i> Document Templates</h5>
</div>
<div class="md:col-span-1">
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 placeholder-neutral-400 dark:placeholder-neutral-500 transition-all duration-200" id="templateSearch" placeholder="Search templates...">
<select class="px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 transition-all duration-200" id="categoryFilter">
<option value="">All Categories</option>
</select>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" id="refreshTemplatesBtn"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</div>
</div>
</div>
<div class="p-6">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden" id="templatesTable">
<thead>
<tr>
<th>Template ID</th>
<th>Name</th>
<th>Category</th>
<th>Variables</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="templatesTableBody">
<!-- Templates will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div id="qdros" class="tabcontent p-6 hidden">
<div class="mt-3 bg-white dark:bg-neutral-800 rounded-xl shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 items-center">
<div><h5 class="mb-0 font-semibold"><i class="fa-regular fa-file-lines"></i> QDRO Documents</h5></div>
<div>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 placeholder-neutral-400 dark:placeholder-neutral-500 transition-all duration-200" id="qdroSearch" placeholder="Search QDROs...">
<select class="px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 transition-all duration-200" id="qdroStatusFilter">
<option value="">All Status</option>
<option value="DRAFT">Draft</option>
<option value="APPROVED">Approved</option>
<option value="FILED">Filed</option>
</select>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" id="refreshQdrosBtn"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</div>
</div>
</div>
<div class="p-6">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden" id="qdrosTable">
<thead>
<tr>
<th>File #</th>
<th>Version</th>
<th>Participant</th>
<th>Spouse</th>
<th>Plan Name</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="qdrosTableBody">
<!-- QDROs will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div id="generated" class="tabcontent p-6 hidden">
<div class="mt-3 bg-white dark:bg-neutral-800 rounded-xl shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700"><h5 class="mb-0 font-semibold"><i class="fa-regular fa-file-pdf"></i> Generated & Uploaded Documents</h5></div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 items-end mb-4">
<div>
<label for="uploadFileNo" class="block text-sm font-medium mb-1">File Number</label>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100" id="uploadFileNo" placeholder="Enter file #">
<button type="button" id="clearUploadFileNoBtn" class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Clear">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div id="uploadFileNoError" class="text-xs text-red-600 mt-1 hidden">Please enter a file number</div>
</div>
<div>
<label for="uploadInput" class="block text-sm font-medium mb-1">Choose File</label>
<input type="file" id="uploadInput" class="block w-full text-sm text-neutral-900 dark:text-neutral-100 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-neutral-100 dark:file:bg-neutral-700 file:text-neutral-700 dark:file:text-neutral-200 hover:file:bg-neutral-200 dark:hover:file:bg-neutral-600" />
<div id="uploadInputError" class="text-xs text-red-600 mt-1 hidden">Please choose a file to upload</div>
</div>
<div class="flex gap-2">
<button type="button" id="uploadBtn" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors"><i class="fa-solid fa-upload mr-2"></i>Upload</button>
<button type="button" id="refreshUploadsBtn" class="px-4 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700 transition-colors"><i class="fa-solid fa-rotate-right mr-2"></i>Refresh</button>
</div>
</div>
<div id="uploadDropZone" class="mt-3 p-6 border-2 border-dashed border-neutral-300 dark:border-neutral-600 rounded-lg text-center text-neutral-500">
<i class="fa-solid fa-cloud-arrow-up text-2xl mb-2"></i>
<div>Drag & drop files here to upload</div>
<div class="text-xs mt-1">or use the chooser above and click Upload</div>
</div>
<div id="uploadingIndicator" class="mt-2 text-sm text-neutral-500 hidden"><i class="fa-solid fa-spinner animate-spin mr-2"></i>Uploading…</div>
<div class="mt-2 flex items-center justify-between">
<div class="flex items-center gap-2 text-xs">
<span class="text-neutral-500">Live updates:</span>
<span id="docLiveBadge"></span>
</div>
<button id="reconnectDocWsBtn" type="button" class="text-xs px-2 py-1 rounded bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 text-neutral-700 dark:text-neutral-300 border border-neutral-300 dark:border-neutral-600">Reconnect</button>
</div>
<div id="uploadProgressList" class="space-y-2 mt-3"></div>
<div id="docEventFeed" class="space-y-2 mt-3" aria-live="polite"></div>
<div id="uploadedDocuments" class="mb-6">
<p class="text-neutral-500">Uploaded documents will appear here.</p>
</div>
<div id="generatedDocuments"><p class="text-neutral-500">Generated documents will appear here...</p></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Template Editor Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="templateModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-5xl w-full max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="templateModalLabel">Template Editor</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('templateModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="px-6 py-4">
<form id="templateForm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div class="md:col-span-1">
<label for="templateId" class="block text-sm font-medium mb-1">Template ID *</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateId" name="form_id" required>
</div>
<div class="md:col-span-1">
<label for="templateName" class="block text-sm font-medium mb-1">Template Name *</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateName" name="form_name" required>
</div>
<div class="md:col-span-2">
<label for="templateCategory" class="block text-sm font-medium mb-1">Category</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateCategory" name="category">
<option value="GENERAL">General</option>
<option value="LETTERS">Letters</option>
<option value="CONTRACTS">Contracts</option>
<option value="PLEADINGS">Pleadings</option>
<option value="FORMS">Forms</option>
<option value="NOTICES">Notices</option>
</select>
</div>
<div class="md:col-span-2">
<label for="templateContent" class="block text-sm font-medium mb-1">Template Content</label>
<div class="mb-2">
<small class="text-neutral-500">
Use {{VARIABLE_NAME}} or ^VARIABLE for merge fields. Available variables: FILE_NO, CLIENT_FIRST, CLIENT_LAST, CLIENT_FULL, MATTER, OPENED, ATTORNEY, TODAY
</small>
</div>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templateContent" name="content" rows="15"
placeholder="Enter your template content here. Use {{CLIENT_FULL}} for client name, {{FILE_NO}} for file number, etc."></textarea>
</div>
<div class="md:col-span-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<button type="button" class="px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700" id="insertVariableBtn">
<i class="fa-solid fa-plus"></i> Insert Variable
</button>
<button type="button" class="px-3 py-1.5 border border-info-600 text-info-700 dark:text-info-300 rounded text-sm hover:bg-info-50 dark:hover:bg-info-900/20" id="previewTemplateBtn">
<i class="fa-regular fa-eye"></i> Preview
</button>
</div>
<div class="text-right">
<div id="variableCount" class="text-neutral-500 text-sm">Variables detected: 0</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('templateModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="saveTemplateBtn"><i class="fa-regular fa-circle-check"></i> Save Template</button>
</div>
</div>
</div>
<!-- Document Generation Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="generateModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="generateModalLabel">Generate Document</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('generateModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="px-6 py-4">
<form id="generateForm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label for="generateTemplate" class="block text-sm font-medium mb-1">Select Template *</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="generateTemplate" name="template_id" required>
<option value="">Choose template...</option>
</select>
</div>
<div>
<label for="generateFile" class="block text-sm font-medium mb-1">File Number *</label>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="generateFile" name="file_no" required>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" type="button" id="selectGenerateFileBtn">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
<div class="text-sm text-neutral-500" id="generateFileInfo">Enter file number or browse to select</div>
</div>
<div>
<label for="outputFormat" class="block text-sm font-medium mb-1">Output Format</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="outputFormat" name="output_format">
<option value="PDF">PDF</option>
<option value="DOCX">Word Document</option>
<option value="HTML">HTML</option>
</select>
</div>
<div>
<div class="mt-6 flex items-center gap-2">
<input class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-neutral-300 rounded" type="checkbox" id="useCustomVars">
<label class="text-sm" for="useCustomVars">
Use custom variables
</label>
</div>
</div>
<div class="md:col-span-2" id="customVariablesSection" style="display: none;">
<label class="block text-sm font-medium mb-1">Custom Variables</label>
<div id="customVariables">
<!-- Custom variables will be added here -->
</div>
<button type="button" class="mt-2 px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700" id="addVariableBtn">
<i class="fa-solid fa-plus"></i> Add Variable
</button>
</div>
<div class="md:col-span-2">
<label for="templatePreview" class="block text-sm font-medium mb-1">Template Preview</label>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="templatePreview" readonly rows="8" placeholder="Select a template to see preview..."></textarea>
</div>
</div>
</form>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('generateModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="generateDocumentBtn"><i class="fa-regular fa-file-lines"></i> Generate Document</button>
</div>
</div>
</div>
<!-- QDRO Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="qdroModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-6xl w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="qdroModalLabel">QDRO Editor</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('qdroModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="px-6 py-4">
<form id="qdroForm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="qdroFileNo" class="block text-sm font-medium mb-1">File Number *</label>
<div class="flex gap-2">
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroFileNo" name="file_no" required>
<button class="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700" type="button" id="selectQdroFileBtn"><i class="fa-solid fa-magnifying-glass"></i></button>
</div>
</div>
<div>
<label for="qdroVersion" class="block text-sm font-medium mb-1">Version</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroVersion" name="version" value="01">
</div>
<div>
<label for="qdroStatus" class="block text-sm font-medium mb-1">Status</label>
<select class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroStatus" name="status">
<option value="DRAFT">Draft</option>
<option value="APPROVED">Approved</option>
<option value="FILED">Filed</option>
</select>
</div>
<div>
<label for="qdroParticipant" class="block text-sm font-medium mb-1">Participant Name</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroParticipant" name="participant_name">
</div>
<div>
<label for="qdroSpouse" class="block text-sm font-medium mb-1">Spouse Name</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroSpouse" name="spouse_name">
</div>
<div>
<label for="qdroPlanName" class="block text-sm font-medium mb-1">Plan Name</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroPlanName" name="plan_name">
</div>
<div>
<label for="qdroPlanAdmin" class="block text-sm font-medium mb-1">Plan Administrator</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroPlanAdmin" name="plan_administrator">
</div>
<div>
<label for="qdroCreated" class="block text-sm font-medium mb-1">Created Date</label>
<input type="date" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroCreated" name="created_date">
</div>
<div>
<label for="qdroApproved" class="block text-sm font-medium mb-1">Approved Date</label>
<input type="date" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroApproved" name="approved_date">
</div>
<div>
<label for="qdroFiled" class="block text-sm font-medium mb-1">Filed Date</label>
<input type="date" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroFiled" name="filed_date">
</div>
<div class="md:col-span-2">
<label for="qdroTitle" class="block text-sm font-medium mb-1">QDRO Title</label>
<input type="text" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroTitle" name="title" placeholder="Enter QDRO title">
</div>
<div class="md:col-span-2">
<label for="qdroContent" class="block text-sm font-medium mb-1">QDRO Content</label>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroContent" name="content" rows="10" placeholder="Enter QDRO content or generate from template..."></textarea>
</div>
<div class="md:col-span-2">
<label for="qdroNotes" class="block text-sm font-medium mb-1">Notes</label>
<textarea class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" id="qdroNotes" name="notes" rows="3" placeholder="Additional notes..."></textarea>
</div>
</div>
</form>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('qdroModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-warning-600 text-white hover:bg-warning-700 rounded-lg" id="generateFromTemplateBtn"><i class="fa-regular fa-file-lines"></i> Generate from Template</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="saveQdroBtn"><i class="fa-regular fa-circle-check"></i> Save QDRO</button>
</div>
</div>
</div>
<!-- Statistics Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="statsModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-3xl w-full max-h-[85vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="statsModalLabel">Document Statistics</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('statsModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<div class="rounded-lg shadow-soft bg-primary-600 text-white p-4 text-center">
<div class="text-3xl font-semibold" id="totalTemplatesCount">0</div>
<div class="text-sm opacity-90">Total Templates</div>
</div>
</div>
<div>
<div class="rounded-lg shadow-soft bg-success-600 text-white p-4 text-center">
<div class="text-3xl font-semibold" id="totalQdrosCount">0</div>
<div class="text-sm opacity-90">Total QDROs</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<div>
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
<h6 class="font-semibold">Templates by Category</h6>
</div>
<div class="p-4">
<div id="categoriesBreakdown">
<!-- Categories will be loaded here -->
</div>
</div>
</div>
</div>
<div>
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-soft border border-neutral-200 dark:border-neutral-700">
<div class="px-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
<h6 class="font-semibold">Recent Activity</h6>
</div>
<div class="p-4">
<div id="recentActivity">
<!-- Recent activity will be loaded here -->
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('statsModal')">Close</button>
</div>
</div>
</div>
<!-- Variable Selector Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="variableModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold" id="variableModalLabel">Insert Variable</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('variableModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-1 gap-2">
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="FILE_NO"><strong>{{FILE_NO}}</strong> - File Number</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="CLIENT_FULL"><strong>{{CLIENT_FULL}}</strong> - Full Client Name</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="CLIENT_FIRST"><strong>{{CLIENT_FIRST}}</strong> - Client First Name</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="CLIENT_LAST"><strong>{{CLIENT_LAST}}</strong> - Client Last Name</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="MATTER"><strong>{{MATTER}}</strong> - Matter Description</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="OPENED"><strong>{{OPENED}}</strong> - Date File Opened</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="ATTORNEY"><strong>{{ATTORNEY}}</strong> - Attorney/Employee</button>
<button type="button" class="px-3 py-2 text-left border rounded hover:bg-neutral-50 dark:hover:bg-neutral-700 variable-item" data-var="TODAY"><strong>{{TODAY}}</strong> - Today's Date</button>
</div>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('variableModal')">Cancel</button>
</div>
</div>
</div>
<!-- Edit Upload Description Modal -->
<div class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" id="editUploadModal">
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-md w-full">
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
<h5 class="text-lg font-semibold">Edit Description</h5>
<button type="button" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300" onclick="closeModal('editUploadModal')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="px-6 py-4">
<input type="hidden" id="editUploadId">
<label for="editUploadDescription" class="block text-sm font-medium mb-1">Description</label>
<textarea id="editUploadDescription" class="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg" rows="4" placeholder="Enter description..."></textarea>
</div>
<div class="flex items-center justify-end gap-2 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700">
<button type="button" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg" onclick="closeModal('editUploadModal')">Cancel</button>
<button type="button" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg" id="saveEditUploadBtn"><i class="fa-regular fa-circle-check"></i> Save</button>
</div>
</div>
</div>
<script>
// Document Management JavaScript
// Example: Upload with correlation-aware alerts
// -------------------------------------------------
// const input = document.querySelector('#uploadInput');
// const fileNo = 'ABC-123';
// const form = new FormData();
// form.append('file', input.files[0]);
// uploadWithAlerts(`/api/documents/upload/${fileNo}`, form)
// .then(() => alerts.success('Upload completed', { duration: 3000 }))
// .catch(() => {/* failure already alerted with Ref: <cid> */});
document.addEventListener('DOMContentLoaded', function() {
// Check authentication first
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/login';
return;
}
// Wait for API helpers to be ready, then initialize
function initializeDocuments() {
if (typeof apiGet === 'function') {
// Ensure API headers are set up with token
// Authorization is injected by window.http.wrappedFetch
// Initialize the first tab as active
document.getElementById('templates-tab').click();
// Load initial data
loadCategories();
// Set up keyboard shortcuts
setupKeyboardShortcuts();
// Set up event handlers
setupEventHandlers();
// Live notifications UI for Generated tab
try { setupGeneratedTabNotifications(); } catch (_) {}
// Auto-refresh every 30 seconds
setInterval(function() {
const templatesTab = document.querySelector('#templates-tab');
const qdrosTab = document.querySelector('#qdros-tab');
if (templatesTab.classList.contains('text-blue-500')) {
loadTemplates();
} else if (qdrosTab.classList.contains('text-blue-500')) {
loadQdros();
}
}, 30000);
} else {
// API helpers not ready yet, try again in 100ms
setTimeout(initializeDocuments, 100);
}
}
initializeDocuments();
});
function setupKeyboardShortcuts() {
document.addEventListener('keydown', function(e) {
// Ctrl+N for new template
if (e.ctrlKey && e.key === 'n') {
e.preventDefault();
document.getElementById('newTemplateBtn').click();
}
// Ctrl+G for generate document
if (e.ctrlKey && e.key === 'g') {
e.preventDefault();
document.getElementById('generateDocBtn').click();
}
});
}
function setupEventHandlers() {
// New template button
document.getElementById('newTemplateBtn').addEventListener('click', function() {
openTemplateModal();
});
// Generate document button
document.getElementById('generateDocBtn').addEventListener('click', function() {
openGenerateModal();
});
// New QDRO button
document.getElementById('newQdroBtn').addEventListener('click', function() {
openQdroModal();
});
// Statistics button
document.getElementById('statsBtn').addEventListener('click', function() {
loadDocumentStats();
});
// Save template button
document.getElementById('saveTemplateBtn').addEventListener('click', function() {
saveTemplate();
});
// Save QDRO button
document.getElementById('saveQdroBtn').addEventListener('click', function() {
saveQdro();
});
// Generate document button
document.getElementById('generateDocumentBtn').addEventListener('click', function() {
generateDocument();
});
// Insert variable button
document.getElementById('insertVariableBtn').addEventListener('click', function() {
openModal('variableModal');
});
// Variable selector
document.querySelectorAll('.variable-item').forEach(item => {
item.addEventListener('click', function() {
const varName = this.dataset.var;
const textarea = document.getElementById('templateContent');
const cursorPos = textarea.selectionStart;
const textBefore = textarea.value.substring(0, cursorPos);
const textAfter = textarea.value.substring(textarea.selectionEnd, textarea.value.length);
textarea.value = textBefore + '{{' + varName + '}}' + textAfter;
textarea.focus();
textarea.setSelectionRange(cursorPos + varName.length + 4, cursorPos + varName.length + 4);
closeModal('variableModal');
updateVariableCount();
});
});
// Template content change handler
document.getElementById('templateContent').addEventListener('input', function() {
updateVariableCount();
});
// Template selection change for generation
document.getElementById('generateTemplate').addEventListener('change', function() {
const templateId = this.value;
if (templateId) {
loadTemplatePreview(templateId);
} else {
document.getElementById('templatePreview').value = '';
}
});
// Custom variables checkbox
document.getElementById('useCustomVars').addEventListener('change', function() {
const section = document.getElementById('customVariablesSection');
section.style.display = this.checked ? 'block' : 'none';
});
// Add custom variable button
document.getElementById('addVariableBtn').addEventListener('click', function() {
addCustomVariableInput();
});
// Search and filter handlers
document.getElementById('templateSearch').addEventListener('input', debounce(loadTemplates, 300));
document.getElementById('categoryFilter').addEventListener('change', loadTemplates);
document.getElementById('qdroSearch').addEventListener('input', debounce(loadQdros, 300));
document.getElementById('qdroStatusFilter').addEventListener('change', loadQdros);
// Refresh buttons
document.getElementById('refreshTemplatesBtn').addEventListener('click', loadTemplates);
document.getElementById('refreshQdrosBtn').addEventListener('click', loadQdros);
const refreshUploadsBtn = document.getElementById('refreshUploadsBtn');
if (refreshUploadsBtn) refreshUploadsBtn.addEventListener('click', loadUploadedDocuments);
const uploadBtn = document.getElementById('uploadBtn');
if (uploadBtn) uploadBtn.addEventListener('click', handleUploadClick);
const saveEditUploadBtn = document.getElementById('saveEditUploadBtn');
if (saveEditUploadBtn) saveEditUploadBtn.addEventListener('click', saveEditUpload);
const dropZone = document.getElementById('uploadDropZone');
if (dropZone) initUploadDropZone(dropZone);
const clearUploadBtn = document.getElementById('clearUploadFileNoBtn');
if (clearUploadBtn) clearUploadBtn.addEventListener('click', clearUploadFileNo);
// Upload controls enable/disable
const fileNoInput = document.getElementById('uploadFileNo');
const uploadInput = document.getElementById('uploadInput');
if (fileNoInput) {
fileNoInput.addEventListener('input', updateUploadControlsState);
fileNoInput.addEventListener('change', updateUploadControlsState);
fileNoInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const fileNo = (fileNoInput.value || '').trim();
const hasFile = !!(uploadInput && uploadInput.files && uploadInput.files.length > 0);
if (fileNo && hasFile) {
const btn = document.getElementById('uploadBtn');
if (btn && btn.disabled) return;
handleUploadClick();
} else if (!hasFile && uploadInput) {
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.remove('hidden');
uploadInput.focus();
}
}
});
}
if (uploadInput) {
uploadInput.addEventListener('change', () => {
updateUploadControlsState();
try {
const fileNoInputEl = document.getElementById('uploadFileNo');
const fileNoVal = (fileNoInputEl?.value || '').trim();
const hasFile = !!(uploadInput && uploadInput.files && uploadInput.files.length > 0);
if (hasFile && !fileNoVal && fileNoInputEl) {
fileNoInputEl.focus();
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.remove('hidden');
shakeElement(fileNoInputEl);
}
} catch (_) {}
});
uploadInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
const hasFile = !!(uploadInput && uploadInput.files && uploadInput.files.length > 0);
if (fileNo && hasFile) {
const btn = document.getElementById('uploadBtn');
if (btn && btn.disabled) return;
handleUploadClick();
} else if (!fileNo) {
const err = document.getElementById('uploadFileNoError');
if (err) err.classList.remove('hidden');
const fileNoInputEl = document.getElementById('uploadFileNo');
if (fileNoInputEl) {
fileNoInputEl.focus();
shakeElement(fileNoInputEl);
}
}
} else if (e.key === 'Escape') {
e.preventDefault();
try {
uploadInput.value = '';
updateUploadControlsState();
uploadInput.focus();
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.add('hidden');
} catch (_) {}
}
});
}
updateUploadControlsState();
// Persist and restore last used upload file number
const fileNoInput = document.getElementById('uploadFileNo');
if (fileNoInput) {
try {
const saved = localStorage.getItem('docs_last_upload_file_no');
if (saved) {
fileNoInput.value = saved;
// Auto-load uploads for restored file number and show one-time hint
try {
if ((saved || '').trim()) {
loadUploadedDocuments()
.then(() => backfillGeneratedForFile(saved))
.then(() => {
try {
if (!sessionStorage.getItem('docs_auto_loaded_hint_shown')) {
showAlert(`Loaded uploads for file ${saved}`, 'info');
sessionStorage.setItem('docs_auto_loaded_hint_shown', '1');
}
} catch (_) {}
});
}
} catch (_) {}
}
const persist = () => {
try { localStorage.setItem('docs_last_upload_file_no', (fileNoInput.value || '').trim()); } catch (_) {}
};
fileNoInput.addEventListener('input', persist);
fileNoInput.addEventListener('change', persist);
} catch (_) {}
}
}
// Live notifications for file-specific events on the Generated tab
function setupGeneratedTabNotifications() {
const badgeHost = document.getElementById('docLiveBadge');
const feed = document.getElementById('docEventFeed');
const reconnectBtn = document.getElementById('reconnectDocWsBtn');
const uploadFileNoInput = document.getElementById('uploadFileNo');
let mgr = null;
let badge = null;
function attachBadge() {
if (!badgeHost || !window.notifications || !window.notifications.createConnectionBadge) return;
const created = window.notifications.createConnectionBadge();
badge = created;
badgeHost.innerHTML = '';
badgeHost.appendChild(created.element);
}
function onEvent(payload) {
if (feed && window.notifications && window.notifications.appendEvent) {
window.notifications.appendEvent(feed, {
fileNo: payload.fileNo,
status: payload.status,
message: payload.data && (payload.data.file_name || payload.data.filename) ? (payload.data.file_name || payload.data.filename) : (payload.message || null),
timestamp: payload.timestamp,
max: 50
});
}
try { updateUploadedBadgeFromEvent(payload); } catch (_) {}
try { upsertGeneratedFromEvent(payload); } catch (_) {}
}
function onState(state) {
try { if (badge && typeof badge.update === 'function') badge.update(state); } catch (_) {}
}
function connectFor(fileNo) {
if (!fileNo || !window.notifications || !window.notifications.connectFileNotifications) return;
if (mgr && typeof mgr.close === 'function') { try { mgr.close(); } catch (_) {} }
try { loadUploadedDocuments(); } catch (_) {}
try { backfillGeneratedForFile(fileNo); } catch (_) {}
mgr = window.notifications.connectFileNotifications({ fileNo, onEvent, onState });
if (!badge) attachBadge();
}
if (reconnectBtn) {
reconnectBtn.addEventListener('click', function(){ if (mgr && typeof mgr.reconnectNow === 'function') mgr.reconnectNow(); });
}
// Connect when a valid file number is present/changes
function maybeConnect() {
const fileNo = (uploadFileNoInput && uploadFileNoInput.value || '').trim();
if (fileNo) connectFor(fileNo);
}
if (uploadFileNoInput) {
uploadFileNoInput.addEventListener('change', maybeConnect);
uploadFileNoInput.addEventListener('blur', maybeConnect);
// initial
maybeConnect();
}
}
// ---------- Status badge helpers ----------
function getStatusBadgeHtml(status) {
const s = String(status || '').toLowerCase();
let cls = 'bg-neutral-100 text-neutral-700 border border-neutral-300';
if (s === 'processing') cls = 'bg-amber-100 text-amber-700 border border-amber-400';
else if (s === 'completed' || s === 'success' || s === 'uploaded' || s === 'ready') cls = 'bg-green-100 text-green-700 border border-green-400';
else if (s === 'failed' || s === 'error') cls = 'bg-red-100 text-red-700 border border-red-400';
const text = (s || 'unknown').toUpperCase();
return `<span class="doc-status-badge inline-block px-2 py-0.5 text-xs rounded ${cls}">${text}</span>`;
}
function updateBadgeElement(el, status) {
if (!el) return;
const wrapper = el.parentElement;
const html = getStatusBadgeHtml(status);
if (window.setSafeHTML) { window.setSafeHTML(wrapper, html); }
else { wrapper.innerHTML = html; }
}
// Update status badge for Uploaded table when matching document_id or filename
function updateUploadedBadgeFromEvent(payload) {
const data = payload && payload.data ? payload.data : {};
const docId = data.document_id != null ? String(data.document_id) : null;
const filename = data.filename || data.file_name || null;
if (!docId && !filename) return;
const container = document.getElementById('uploadedDocuments');
if (!container) return;
let row = null;
if (docId) {
row = container.querySelector(`tr[data-doc-id="${CSS.escape(String(docId))}"]`);
}
if (!row && filename) {
row = container.querySelector(`tr[data-filename="${CSS.escape(String(filename))}"]`);
}
if (!row) return;
const badge = row.querySelector('.doc-status-badge');
if (!badge) return;
const status = (data && data.action === 'upload') ? 'uploaded' : payload.status;
updateBadgeElement(badge, status);
}
// Ensure generated documents table exists
function ensureGeneratedTable() {
const container = document.getElementById('generatedDocuments');
if (!container) return null;
// If already a table, return tbody
let tbody = container.querySelector('#generatedDocsTableBody');
if (tbody) return tbody;
const html = `
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<thead>
<tr>
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">Size</th>
</tr>
</thead>
<tbody id="generatedDocsTableBody"></tbody>
</table>
`;
if (window.setSafeHTML) { window.setSafeHTML(container, html); }
else { container.innerHTML = html; }
return container.querySelector('#generatedDocsTableBody');
}
// Create or update a generated doc row
function upsertGeneratedFromEvent(payload) {
const status = String(payload && payload.status || '').toLowerCase();
if (!status) return;
const data = payload && payload.data ? payload.data : {};
const tbody = ensureGeneratedTable();
if (!tbody) return;
const fileNo = payload.fileNo || data.file_no || '';
const docId = data.document_id != null ? String(data.document_id) : null;
const filename = data.filename || data.file_name || (data.template_name ? `${data.template_name} (${fileNo})` : null);
const size = data.size != null ? Number(data.size) : null;
const keySelector = docId ? `tr[data-doc-id="${CSS.escape(docId)}"]` : (filename ? `tr[data-filename="${CSS.escape(filename)}"]` : null);
let row = keySelector ? tbody.querySelector(keySelector) : null;
if (!row) {
row = document.createElement('tr');
if (docId) row.setAttribute('data-doc-id', String(docId));
if (filename) row.setAttribute('data-filename', String(filename));
const nameCell = document.createElement('td');
nameCell.className = 'px-4 py-2';
nameCell.textContent = filename || '[Unknown]';
const statusCell = document.createElement('td');
statusCell.className = 'px-4 py-2';
if (window.setSafeHTML) { window.setSafeHTML(statusCell, getStatusBadgeHtml(status)); }
else { statusCell.innerHTML = getStatusBadgeHtml(status); }
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-2';
sizeCell.textContent = size != null ? `${Number(size).toLocaleString()} bytes` : '';
row.appendChild(nameCell);
row.appendChild(statusCell);
row.appendChild(sizeCell);
tbody.prepend(row);
} else {
const badge = row.querySelector('.doc-status-badge');
if (badge) updateBadgeElement(badge, status);
const sizeCell = row.children[2];
if (size != null && sizeCell) sizeCell.textContent = `${Number(size).toLocaleString()} bytes`;
}
}
// Backfill current generated documents for a file before live updates begin
async function backfillGeneratedForFile(fileNo) {
try {
if (!fileNo) return;
// 1) Status backfill for processing badge
try {
const statusResp = await window.http.wrappedFetch(`/api/documents/current-status/${encodeURIComponent(fileNo)}`);
if (statusResp && statusResp.ok) {
const st = await statusResp.json();
if (st && String(st.status || '').toLowerCase() === 'processing') {
// Surface a processing row in Generated section for immediate feedback
upsertGeneratedFromEvent({ fileNo, status: 'processing', data: (st.data || {}) });
}
}
} catch (_) {}
// 2) Seed existing generated docs from uploaded list
const resp = await window.http.wrappedFetch(`/api/documents/${encodeURIComponent(fileNo)}/uploaded`);
if (!resp.ok) { return; }
const docs = await resp.json();
const generated = Array.isArray(docs) ? docs.filter((d) => String(d.description || '').toLowerCase().includes('generated')) : [];
if (!generated.length) return;
for (const d of generated) {
try {
upsertGeneratedFromEvent({
fileNo,
status: 'completed',
data: { document_id: d.id, filename: d.filename, size: d.size }
});
} catch (_) {}
}
} catch (_) {}
}
function updateUploadControlsState() {
try {
const btn = document.getElementById('uploadBtn');
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
const input = document.getElementById('uploadInput');
const hasFile = !!(input && input.files && input.files.length > 0);
const enabled = !!fileNo && hasFile;
if (btn) {
btn.disabled = !enabled;
btn.classList.toggle('opacity-50', !enabled);
btn.classList.toggle('cursor-not-allowed', !enabled);
btn.setAttribute('aria-disabled', String(!enabled));
}
// Inline error messages
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.toggle('hidden', !!fileNo);
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.toggle('hidden', hasFile);
} catch (_) {}
}
function shakeElement(el) {
try {
el.classList.add('animate-shake');
setTimeout(() => el.classList.remove('animate-shake'), 400);
} catch (_) {}
}
function clearUploadFileNo() {
try {
const input = document.getElementById('uploadFileNo');
if (input) input.value = '';
try { localStorage.removeItem('docs_last_upload_file_no'); } catch (_) {}
const container = document.getElementById('uploadedDocuments');
if (container) container.innerHTML = '<p class="text-neutral-500">No uploads found for this file.</p>';
const gen = document.getElementById('generatedDocuments');
if (gen) gen.innerHTML = '<p class="text-neutral-500">Generated documents will appear here...</p>';
} catch (_) {}
}
// Authorization and JSON headers are injected by window.http.wrappedFetch
async function loadTemplates() {
try {
// Loading templates...
const search = document.getElementById('templateSearch').value;
const category = document.getElementById('categoryFilter').value;
let url = '/api/documents/templates/?';
if (search) url += `search=${encodeURIComponent(search)}&`;
if (category) url += `category=${encodeURIComponent(category)}&`;
// Ensure we have auth token for this API call
const token = localStorage.getItem('auth_token');
// Token validation
if (!token) {
// No token found, redirecting to login
window.location.href = '/login';
return;
}
// Making API call
const response = await window.http.wrappedFetch(url);
// Processing response
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
const templates = await response.json();
// Templates loaded successfully
displayTemplates(templates);
} catch (error) {
console.error('Error in loadTemplates:', error);
try { logClientError({ message: 'Error loading templates', action: 'loadTemplates', error }); } catch (_) {}
showAlert('Error loading templates: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}
}
function initUploadDropZone(zoneEl) {
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter','dragover','dragleave','drop'].forEach(eventName => {
zoneEl.addEventListener(eventName, preventDefaults, false);
});
zoneEl.addEventListener('dragover', () => zoneEl.classList.add('bg-neutral-50', 'dark:bg-neutral-900/20'));
zoneEl.addEventListener('dragleave', () => zoneEl.classList.remove('bg-neutral-50', 'dark:bg-neutral-900/20', 'border-red-400'));
zoneEl.addEventListener('drop', async (e) => {
zoneEl.classList.remove('bg-neutral-50', 'dark:bg-neutral-900/20', 'border-red-400');
const dt = e.dataTransfer;
const files = dt && dt.files ? Array.from(dt.files) : [];
if (!files.length) return;
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
if (!fileNo) {
zoneEl.classList.add('border-red-400');
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.remove('hidden');
showAlert('Please enter a file number', 'warning');
return;
}
try { localStorage.setItem('docs_last_upload_file_no', fileNo); } catch (_) {}
setUploadingState(true);
await concurrentUploads(files, fileNo, 3);
setUploadingState(false);
loadUploadedDocuments();
});
}
function createUploadItem(file) {
const list = document.getElementById('uploadProgressList');
const item = document.createElement('div');
item.className = 'flex items-center justify-between p-3 border rounded-lg';
const name = document.createElement('div');
name.className = 'text-sm font-medium truncate max-w-[60%]';
name.textContent = file.name;
const right = document.createElement('div');
right.className = 'flex items-center gap-3';
const status = document.createElement('span');
status.className = 'text-xs text-neutral-500';
status.textContent = 'Queued';
const barWrap = document.createElement('div');
barWrap.className = 'w-40 h-2 bg-neutral-200 dark:bg-neutral-700 rounded overflow-hidden';
const bar = document.createElement('div');
bar.className = 'h-2 bg-primary-500 w-0 transition-all';
bar.style.width = '0%';
barWrap.appendChild(bar);
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.title = 'Cancel upload';
cancelBtn.className = 'px-2 py-1 border border-neutral-400 text-neutral-600 rounded hover:bg-neutral-100 text-xs';
cancelBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
cancelBtn.disabled = true;
right.appendChild(status);
right.appendChild(barWrap);
right.appendChild(cancelBtn);
item.appendChild(name);
item.appendChild(right);
list.appendChild(item);
let abortFn = null;
cancelBtn.addEventListener('click', () => {
if (typeof abortFn === 'function') {
status.textContent = 'Cancelling…';
cancelBtn.disabled = true;
abortFn();
}
});
return { item, status, bar, cancelBtn, setAbort: (fn) => { abortFn = fn; cancelBtn.disabled = !fn; } };
}
function setUploadingState(isUploading) {
try {
const uploadBtn = document.getElementById('uploadBtn');
const dropZone = document.getElementById('uploadDropZone');
const indicator = document.getElementById('uploadingIndicator');
if (uploadBtn) uploadBtn.disabled = !!isUploading;
if (dropZone) dropZone.classList.toggle('opacity-50', !!isUploading);
if (dropZone) dropZone.classList.toggle('pointer-events-none', !!isUploading);
if (indicator) indicator.classList.toggle('hidden', !isUploading);
} catch (_) {}
}
async function concurrentUploads(files, fileNo, concurrency = 3) {
const queue = Array.from(files);
const active = new Set();
let completed = 0;
function startNext() {
if (queue.length === 0 || active.size >= concurrency) return null;
const file = queue.shift();
const ui = createUploadItem(file);
const controller = new AbortController();
ui.setAbort(() => controller.abort());
const task = (async () => {
try {
ui.status.textContent = 'Uploading…';
ui.bar.style.width = '25%';
const form = new FormData();
form.append('file', file);
await uploadWithAlerts(`/api/documents/upload/${encodeURIComponent(fileNo)}`, form, { alertTitle: 'Upload failed', extraOptions: { signal: controller.signal } });
ui.bar.style.width = '100%';
ui.status.textContent = 'Done';
ui.status.className = 'text-xs text-green-600';
} catch (err) {
if (err && (err.name === 'AbortError' || /aborted/i.test(String(err && err.message)))) {
ui.status.textContent = 'Canceled';
ui.status.className = 'text-xs text-yellow-600';
ui.bar.style.width = '100%';
ui.bar.classList.remove('bg-primary-500');
ui.bar.classList.add('bg-yellow-500');
} else {
ui.status.textContent = 'Failed';
ui.status.className = 'text-xs text-red-600';
ui.bar.style.width = '100%';
ui.bar.classList.remove('bg-primary-500');
ui.bar.classList.add('bg-red-500');
// Keep failed item visible and allow dismiss
try {
ui.item.classList.add('bg-red-50');
ui.item.classList.add('border');
ui.item.classList.add('border-red-300');
ui.cancelBtn.disabled = false;
ui.cancelBtn.title = 'Dismiss';
ui.cancelBtn.addEventListener('click', () => {
try {
ui.item.style.transition = 'opacity 250ms ease-in-out';
ui.item.style.opacity = '0';
setTimeout(() => ui.item.remove(), 260);
} catch (_) { ui.item.remove(); }
});
} catch (_) {}
}
} finally {
completed += 1;
active.delete(task);
ui.setAbort(null);
startNext();
}
})();
active.add(task);
return task;
}
// Kick off initial batch
const starters = [];
for (let i = 0; i < concurrency; i++) {
const t = startNext();
if (t) starters.push(t);
}
await Promise.allSettled(starters);
// Drain the rest
while (active.size > 0 || queue.length > 0) {
if (queue.length > 0) startNext();
await Promise.race(Array.from(active));
}
const anyFailed = Array.from(document.querySelectorAll('#uploadProgressList .text-red-600')).length > 0;
if (!anyFailed) {
showAlert('All uploads completed', 'success');
}
cleanupUploadProgress(true);
}
function cleanupUploadProgress(preserveFailures = true) {
try {
const list = document.getElementById('uploadProgressList');
if (!list) return;
const items = Array.from(list.children);
items.forEach((item) => {
try {
const statusEl = item.querySelector('span.text-xs');
const isFailed = preserveFailures && statusEl && statusEl.classList.contains('text-red-600');
if (isFailed) {
// Ensure a dismiss button exists
const hasDismiss = item.querySelector('button');
if (!hasDismiss) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'px-2 py-1 border border-neutral-400 text-neutral-600 rounded hover:bg-neutral-100 text-xs';
btn.title = 'Dismiss';
btn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
btn.addEventListener('click', () => {
try {
item.style.transition = 'opacity 250ms ease-in-out';
item.style.opacity = '0';
setTimeout(() => item.remove(), 260);
} catch (_) { item.remove(); }
});
const right = item.querySelector('.flex.items-center.gap-3');
if (right) right.appendChild(btn);
}
return;
}
item.style.transition = 'opacity 250ms ease-in-out';
item.style.opacity = '0';
setTimeout(() => item.remove(), 260);
} catch (_) {}
});
} catch (_) {}
}
function displayTemplates(templates) {
const tbody = document.getElementById('templatesTableBody');
tbody.innerHTML = '';
templates.forEach(template => {
const row = createTemplateRow(template);
tbody.appendChild(row);
});
}
function createTemplateRow(template) {
const row = document.createElement('tr');
const variableCount = Object.keys(template.variables || {}).length;
const rowHtml = `
<td class="px-4 py-2"><code>${template.form_id}</code></td>
<td class="px-4 py-2">${template.form_name}</td>
<td class="px-4 py-2"><span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-100 text-neutral-700 border border-neutral-300">${template.category}</span></td>
<td class="px-4 py-2"><span class="inline-block px-2 py-0.5 text-xs rounded bg-blue-100 text-blue-700 border border-blue-400">${variableCount} vars</span></td>
<td class="px-4 py-2">
<span class="inline-block px-2 py-0.5 text-xs rounded ${template.active ? 'bg-green-100 text-green-700 border border-green-400' : 'bg-yellow-100 text-yellow-700 border border-yellow-500'}">
${template.active ? 'Active' : 'Inactive'}
</span>
</td>
<td class="px-4 py-2">
<div class="flex items-center gap-2">
<button class="px-2 py-1 border border-primary-600 text-primary-600 rounded hover:bg-blue-100" onclick="editTemplate('${template.form_id}')" title="Edit"><i class="fa-solid fa-pencil"></i></button>
<button class="px-2 py-1 border border-success-600 text-success-600 rounded hover:bg-green-100" onclick="previewTemplate('${template.form_id}')" title="Preview"><i class="fa-regular fa-eye"></i></button>
<button class="px-2 py-1 border border-info-600 text-info-600 rounded hover:bg-blue-100" onclick="generateFromTemplate('${template.form_id}')" title="Generate"><i class="fa-regular fa-file-lines"></i></button>
<button class="px-2 py-1 border border-danger-600 text-danger-600 rounded hover:bg-red-100" onclick="deleteTemplate('${template.form_id}')" title="Delete"><i class="fa-solid fa-trash"></i></button>
</div>
</td>
`;
if (window.setSafeHTML) { window.setSafeHTML(row, rowHtml); } else { row.innerHTML = rowHtml; }
return row;
}
async function loadQdros() {
try {
const search = document.getElementById('qdroSearch').value;
const status = document.getElementById('qdroStatusFilter').value;
let url = '/api/documents/qdros/?';
if (search) url += `search=${encodeURIComponent(search)}&`;
if (status) url += `status_filter=${encodeURIComponent(status)}&`;
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/login';
return;
}
const response = await window.http.wrappedFetch(url);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
const qdros = await response.json();
displayQdros(qdros);
} catch (error) {
console.error('Error loading QDROs:', error);
try { logClientError({ message: 'Error loading QDROs', action: 'loadQdros', error }); } catch (_) {}
showAlert('Error loading QDROs: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}
}
function displayQdros(qdros) {
const tbody = document.getElementById('qdrosTableBody');
tbody.innerHTML = '';
qdros.forEach(qdro => {
const row = createQdroRow(qdro);
tbody.appendChild(row);
});
}
function createQdroRow(qdro) {
const row = document.createElement('tr');
const qdroRowHtml = `
<td><code>${qdro.file_no}</code></td>
<td>${qdro.version}</td>
<td>${qdro.participant_name || ''}</td>
<td>${qdro.spouse_name || ''}</td>
<td>${qdro.plan_name || ''}</td>
<td>
<span class="${getStatusBadgeClass(qdro.status)}">
${qdro.status}
</span>
</td>
<td>${qdro.created_date ? new Date(qdro.created_date).toLocaleDateString() : ''}</td>
<td>
<div class="flex items-center gap-2">
<button class="px-2 py-1 border border-blue-600 text-blue-600 rounded hover:bg-blue-100" onclick="editQdro(${qdro.id})" title="Edit"><i class="fa-solid fa-pencil"></i></button>
<button class="px-2 py-1 border border-cyan-600 text-cyan-600 rounded hover:bg-blue-100" onclick="viewQdro(${qdro.id})" title="View"><i class="fa-regular fa-eye"></i></button>
<button class="px-2 py-1 border border-red-600 text-red-600 rounded hover:bg-red-100" onclick="deleteQdro(${qdro.id})" title="Delete"><i class="fa-solid fa-trash"></i></button>
</div>
</td>
`;
if (window.setSafeHTML) { window.setSafeHTML(row, qdroRowHtml); } else { row.innerHTML = qdroRowHtml; }
return row;
}
function getStatusBadgeClass(status) {
switch (status) {
case 'DRAFT': return 'inline-block px-2 py-0.5 text-xs rounded bg-yellow-100 text-yellow-700 border border-yellow-500';
case 'APPROVED': return 'inline-block px-2 py-0.5 text-xs rounded bg-green-100 text-green-700 border border-green-400';
case 'FILED': return 'inline-block px-2 py-0.5 text-xs rounded bg-blue-100 text-blue-700 border border-blue-400';
default: return 'inline-block px-2 py-0.5 text-xs rounded bg-neutral-100 text-neutral-700 border border-neutral-300';
}
}
async function loadCategories() {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/login';
return;
}
const response = await window.http.wrappedFetch('/api/documents/categories/');
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
const categories = await response.json();
const select = document.getElementById('categoryFilter');
const templateSelect = document.getElementById('templateCategory');
// Clear existing options except "All Categories"
select.innerHTML = '<option value="">All Categories</option>';
categories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading categories:', error);
try { logClientError({ message: 'Error loading categories', action: 'loadCategories', error }); } catch (_) {}
}
}
function openTemplateModal(templateId = null) {
const form = document.getElementById('templateForm');
// Reset form
form.reset();
document.getElementById('templateModalLabel').textContent = templateId ? 'Edit Template' : 'New Template';
if (templateId) {
loadTemplateForEditing(templateId);
} else {
// Set today's date and generate ID
const today = new Date().toISOString().split('T')[0];
document.getElementById('templateId').value = 'TPL_' + Date.now();
}
openModal('templateModal');
updateVariableCount();
}
async function loadTemplateForEditing(templateId) {
try {
const response = await window.http.wrappedFetch(`/api/documents/templates/${templateId}`);
if (!response.ok) throw new Error('Failed to load template');
const template = await response.json();
document.getElementById('templateId').value = template.form_id;
document.getElementById('templateId').disabled = true; // Don't allow editing ID
document.getElementById('templateName').value = template.form_name;
document.getElementById('templateCategory').value = template.category;
document.getElementById('templateContent').value = template.content;
updateVariableCount();
} catch (error) {
console.error('Error loading template:', error);
try { logClientError({ message: 'Error loading template for edit', action: 'loadTemplateForEditing', error, extra: { templateId } }); } catch (_) {}
showAlert('Error loading template: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}
}
async function saveTemplate() {
try {
const form = document.getElementById('templateForm');
const formData = new FormData(form);
const templateData = {
form_id: formData.get('form_id'),
form_name: formData.get('form_name'),
category: formData.get('category'),
content: formData.get('content')
};
const isEdit = document.getElementById('templateId').disabled;
const url = isEdit ? `/api/documents/templates/${templateData.form_id}` : '/api/documents/templates/';
const method = isEdit ? 'PUT' : 'POST';
const response = await window.http.wrappedFetch(url, {
method: method,
body: JSON.stringify(templateData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to save template');
}
showAlert('Template saved successfully', 'success');
closeModal('templateModal');
loadTemplates();
} catch (error) {
console.error('Error saving template:', error);
try { logClientError({ message: 'Error saving template', action: 'saveTemplate', error }); } catch (_) {}
showAlert('Error saving template: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}
}
function updateVariableCount() {
const content = document.getElementById('templateContent').value;
const variables = extractVariables(content);
document.getElementById('variableCount').textContent = `Variables detected: ${variables.length}`;
}
function extractVariables(content) {
const regex = /\{\{([^}]+)\}\}/g;
const variables = [];
let match;
while ((match = regex.exec(content)) !== null) {
if (!variables.includes(match[1])) {
variables.push(match[1]);
}
}
return variables;
}
async function loadDocumentStats() {
try {
const response = await window.http.wrappedFetch('/api/documents/stats/summary');
if (!response.ok) throw new Error('Failed to load statistics');
const stats = await response.json();
document.getElementById('totalTemplatesCount').textContent = stats.total_templates;
document.getElementById('totalQdrosCount').textContent = stats.total_qdros;
// Display categories breakdown
const categoriesDiv = document.getElementById('categoriesBreakdown');
categoriesDiv.innerHTML = '';
Object.entries(stats.templates_by_category).forEach(([category, count]) => {
const div = document.createElement('div');
div.className = 'flex items-center justify-between mb-1';
const html = `<span>${category}</span><span class="inline-block px-2 py-0.5 text-xs rounded bg-neutral-200 text-neutral-700">${count}</span>`;
if (window.setSafeHTML) { window.setSafeHTML(div, html); } else { div.innerHTML = html; }
categoriesDiv.appendChild(div);
});
// Display recent activity
const activityDiv = document.getElementById('recentActivity');
activityDiv.innerHTML = '';
if (stats.recent_activity.length === 0) {
activityDiv.innerHTML = '<p class="text-neutral-500">No recent activity</p>';
} else {
stats.recent_activity.forEach(activity => {
const div = document.createElement('div');
div.className = 'mb-2 p-2 border rounded';
const activityHtml = `
<small class="text-neutral-500">${activity.type}</small><br>
<strong>File: ${activity.file_no}</strong><br>
<span class="${getStatusBadgeClass(activity.status)}">${activity.status}</span>
`;
if (window.setSafeHTML) { window.setSafeHTML(div, activityHtml); } else { div.innerHTML = activityHtml; }
activityDiv.appendChild(div);
});
}
openModal('statsModal');
} catch (error) {
console.error('Error loading statistics:', error);
try { logClientError({ message: 'Error loading statistics', action: 'loadDocumentStats', error }); } catch (_) {}
showAlert('Error loading statistics: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}
}
// Utility functions
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function showAlert(message, type = 'info') {
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(message, type);
} else if (window.showNotification) {
window.showNotification(message, type);
} else {
alert(String(message));
}
}
// Uploads UI
async function loadUploadedDocuments() {
try {
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
if (!fileNo) {
showAlert('Enter a file number to view uploads', 'warning');
return;
}
const resp = await window.http.wrappedFetch(`/api/documents/${encodeURIComponent(fileNo)}/uploaded`);
if (!resp.ok) {
const err = await window.http.toError(resp, 'Failed to load uploads');
const msg = window.http.formatAlert(err, 'Failed to load uploads');
showAlert(msg, 'danger');
return;
}
const docs = await resp.json();
displayUploadedDocuments(docs);
} catch (error) {
console.error('Error loading uploaded documents', error);
showAlert('Failed to load uploads', 'danger');
}
}
function displayUploadedDocuments(docs) {
const container = document.getElementById('uploadedDocuments');
if (!Array.isArray(docs) || docs.length === 0) {
container.innerHTML = '<p class="text-neutral-500">No uploads found for this file.</p>';
return;
}
const rows = docs.map((d) => `
<tr data-doc-id="${String(d.id || '')}" data-filename="${String(d.filename || '').replace(/"/g, '&quot;')}">
<td class="px-4 py-2">${d.id || ''}</td>
<td class="px-4 py-2">${d.filename || ''}</td>
<td class="px-4 py-2">${(d.type || '').split('/').pop()}</td>
<td class="px-4 py-2">${Number(d.size || 0).toLocaleString()} bytes</td>
<td class="px-4 py-2"><span class="doc-status-badge inline-block px-2 py-0.5 text-xs rounded bg-green-100 text-green-700 border border-green-400">UPLOADED</span></td>
<td class="px-4 py-2"><a href="/${d.path || ''}" target="_blank" class="text-primary-600 hover:underline">View</a></td>
<td class="px-4 py-2">
<button class="px-2 py-1 border border-cyan-600 text-cyan-600 rounded hover:bg-blue-100 mr-2" title="Edit description" onclick="openEditUploadModal(${JSON.stringify(String(d.id || ''))}, ${JSON.stringify(String(d.description || ''))})"><i class="fa-solid fa-pencil"></i></button>
<button class="px-2 py-1 border border-danger-600 text-danger-600 rounded hover:bg-red-100" title="Delete" onclick="deleteUploadedDocument(${JSON.stringify(String(d.id || ''))})"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
`).join('');
const html = `
<table class="w-full text-sm text-left border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<thead>
<tr>
<th class="px-4 py-2">ID</th>
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Type</th>
<th class="px-4 py-2">Size</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">Link</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
`;
if (window.setSafeHTML) { window.setSafeHTML(container, html); } else { container.innerHTML = html; }
}
async function handleUploadClick() {
const fileNo = (document.getElementById('uploadFileNo')?.value || '').trim();
const input = document.getElementById('uploadInput');
if (!fileNo) {
showAlert('Please enter a file number', 'warning');
const fileNoErr = document.getElementById('uploadFileNoError');
if (fileNoErr) fileNoErr.classList.remove('hidden');
return;
}
try { localStorage.setItem('docs_last_upload_file_no', fileNo); } catch (_) {}
if (!input || !input.files || input.files.length === 0) {
showAlert('Please choose a file to upload', 'warning');
const inputErr = document.getElementById('uploadInputError');
if (inputErr) inputErr.classList.remove('hidden');
return;
}
const form = new FormData();
form.append('file', input.files[0]);
setUploadingState(true);
try {
await uploadWithAlerts(`/api/documents/upload/${encodeURIComponent(fileNo)}`, form);
if (window.alerts && window.alerts.success) {
window.alerts.success('Upload completed', { duration: 3000 });
} else {
showAlert('Upload completed', 'success');
}
// refresh list
loadUploadedDocuments();
// clear chooser
input.value = '';
} catch (_) {
// Error already alerted by helper
} finally {
setUploadingState(false);
}
}
async function deleteUploadedDocument(docId) {
try {
if (!docId) {
showAlert('Invalid document id', 'warning');
return;
}
if (!confirm('Are you sure you want to delete this uploaded document?')) return;
const resp = await window.http.wrappedFetch(`/api/documents/uploaded/${encodeURIComponent(String(docId))}`, { method: 'DELETE' });
if (!resp.ok) {
const err = await window.http.toError(resp, 'Failed to delete upload');
const msg = window.http.formatAlert(err, 'Failed to delete upload');
showAlert(msg, 'danger');
return;
}
showAlert('Upload deleted successfully', 'success');
loadUploadedDocuments();
} catch (error) {
const msg = window.http && window.http.formatAlert && error instanceof Error
? window.http.formatAlert(error, 'Failed to delete upload')
: 'Failed to delete upload';
showAlert(msg, 'danger');
}
}
function openEditUploadModal(docId, currentDescription) {
try {
document.getElementById('editUploadId').value = String(docId || '');
document.getElementById('editUploadDescription').value = String(currentDescription || '');
openModal('editUploadModal');
} catch (error) {
showAlert('Unable to open editor', 'danger');
}
}
async function saveEditUpload() {
const docId = document.getElementById('editUploadId').value;
const description = document.getElementById('editUploadDescription').value;
if (!docId) {
showAlert('Invalid document id', 'warning');
return;
}
try {
const form = new FormData();
form.append('description', description || '');
const resp = await window.http.wrappedFetch(`/api/documents/uploaded/${encodeURIComponent(String(docId))}`, {
method: 'PUT',
body: form
});
if (!resp.ok) {
const err = await window.http.toError(resp, 'Failed to update description');
const msg = window.http.formatAlert(err, 'Failed to update description');
showAlert(msg, 'danger');
return;
}
showAlert('Description updated', 'success');
closeModal('editUploadModal');
loadUploadedDocuments();
} catch (error) {
const msg = window.http && window.http.formatAlert && error instanceof Error
? window.http.formatAlert(error, 'Failed to update description')
: 'Failed to update description';
showAlert(msg, 'danger');
}
}
// Lightweight client error logger specific to Documents page
async function logClientError({ message, action = null, error = null, extra = null }) {
try {
const payload = {
message: String(message || (error && error.message) || 'Unknown error'),
action,
stack: error && error.stack ? String(error.stack) : null,
url: window.location.href,
user_agent: navigator.userAgent,
extra
};
await window.http.wrappedFetch('/api/documents/client-error', {
method: 'POST',
body: JSON.stringify(payload)
});
} catch (_) {
// swallow
}
}
// Placeholder functions for additional features
async function editTemplate(templateId) {
openTemplateModal(templateId);
}
async function previewTemplate(templateId) {
// Implement template preview
}
async function generateFromTemplate(templateId) {
document.getElementById('generateTemplate').value = templateId;
openGenerateModal();
}
async function deleteTemplate(templateId) {
if (confirm('Are you sure you want to delete this template?')) {
try {
const response = await window.http.wrappedFetch(`/api/documents/templates/${templateId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete template');
showAlert('Template deleted successfully', 'success');
loadTemplates();
} catch (error) {
console.error('Error deleting template:', error);
try { logClientError({ message: 'Error deleting template', action: 'deleteTemplate', error, extra: { templateId } }); } catch (_) {}
showAlert('Error deleting template: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}
}
}
function openGenerateModal() {
loadTemplatesForGeneration();
openModal('generateModal');
}
async function loadTemplatesForGeneration() {
try {
const response = await window.http.wrappedFetch('/api/documents/templates/');
if (!response.ok) throw new Error('Failed to load templates');
const templates = await response.json();
const select = document.getElementById('generateTemplate');
select.innerHTML = '<option value="">Choose template...</option>';
templates.forEach(template => {
const option = document.createElement('option');
option.value = template.form_id;
option.textContent = `${template.form_name} (${template.category})`;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading templates:', error);
try { logClientError({ message: 'Error loading templates for generation', action: 'loadTemplatesForGeneration', error }); } catch (_) {}
}
}
async function loadTemplatePreview(templateId) {
try {
const response = await window.http.wrappedFetch(`/api/documents/templates/${templateId}`);
if (!response.ok) throw new Error('Failed to load template');
const template = await response.json();
document.getElementById('templatePreview').value = template.content;
} catch (error) {
console.error('Error loading template preview:', error);
try { logClientError({ message: 'Error loading template preview', action: 'loadTemplatePreview', error, extra: { templateId } }); } catch (_) {}
}
}
async function generateDocument() {
try {
const form = document.getElementById('generateForm');
const formData = new FormData(form);
const templateId = formData.get('template_id');
if (!templateId) {
showAlert('Please select a template', 'warning');
return;
}
const requestData = {
template_id: templateId,
file_no: formData.get('file_no'),
output_format: formData.get('output_format'),
variables: {}
};
// Add custom variables if any
if (document.getElementById('useCustomVars').checked) {
const customVarInputs = document.querySelectorAll('#customVariables .custom-var-input');
customVarInputs.forEach(input => {
const name = input.querySelector('.var-name').value;
const value = input.querySelector('.var-value').value;
if (name && value) {
requestData.variables[name] = value;
}
});
}
const response = await window.http.wrappedFetch(`/api/documents/generate/${templateId}`, {
method: 'POST',
body: JSON.stringify(requestData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to generate document');
}
const result = await response.json();
showAlert(`Document generated successfully: ${result.file_name}`, 'success');
closeModal('generateModal');
// Optionally trigger download
if (confirm('Document generated successfully. Download now?')) {
window.open(result.file_path, '_blank');
}
} catch (error) {
console.error('Error generating document:', error);
try { logClientError({ message: 'Error generating document', action: 'generateDocument', error, extra: { templateId: (document.getElementById('generateTemplate')?.value || null) } }); } catch (_) {}
showAlert('Error generating document: ' + (error && error.message ? error.message : 'Unknown error'), 'danger');
}
}
function addCustomVariableInput() {
const container = document.getElementById('customVariables');
const div = document.createElement('div');
div.className = 'grid grid-cols-12 gap-2 mb-2 custom-var-input';
const customVarHtml = `
<div class="col-span-12 md:col-span-5">
<input type="text" class="w-full px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded var-name" placeholder="Variable name">
</div>
<div class="col-span-12 md:col-span-5">
<input type="text" class="w-full px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded var-value" placeholder="Variable value">
</div>
<div class="col-span-12 md:col-span-2 flex items-center">
<button type="button" class="px-2 py-1 border border-red-600 text-red-600 rounded hover:bg-red-100 text-sm" onclick="this.closest('.custom-var-input').remove()">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
`;
if (window.setSafeHTML) { window.setSafeHTML(div, customVarHtml); } else { div.innerHTML = customVarHtml; }
container.appendChild(div);
}
function openQdroModal(qdroId = null) {
const form = document.getElementById('qdroForm');
form.reset();
document.getElementById('qdroModalLabel').textContent = qdroId ? 'Edit QDRO' : 'New QDRO';
if (!qdroId) {
document.getElementById('qdroCreated').value = new Date().toISOString().split('T')[0];
}
openModal('qdroModal');
}
async function saveQdro() {
// Implement QDRO save functionality
showAlert('QDRO functionality will be implemented in the next phase', 'info');
}
async function editQdro(qdroId) {
openQdroModal(qdroId);
}
async function viewQdro(qdroId) {
// Implement QDRO view functionality
showAlert('QDRO view functionality will be implemented', 'info');
}
async function deleteQdro(qdroId) {
if (confirm('Are you sure you want to delete this QDRO?')) {
// Implement delete functionality
showAlert('QDRO delete functionality will be implemented', 'info');
}
}
// Tab navigation functionality
function openTab(evt, tabName) {
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].classList.add('hidden');
}
// Get all elements with class="tablinks" and remove the "active" class
tablinks = document.querySelectorAll('nav button');
for (i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove('border-blue-500', 'text-blue-500', 'dark:text-blue-400');
tablinks[i].classList.add('border-transparent');
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabName).classList.remove('hidden');
evt.currentTarget.classList.add('border-blue-500', 'text-blue-500', 'dark:text-blue-400');
evt.currentTarget.classList.remove('border-transparent');
// Load data for the active tab
if (tabName === 'templates') {
loadTemplates();
} else if (tabName === 'qdros') {
loadQdros();
} else if (tabName === 'generated') {
const fileNoInput = document.getElementById('uploadFileNo');
const uploadInput = document.getElementById('uploadInput');
if (fileNoInput && (fileNoInput.value || '').trim() && uploadInput) {
uploadInput.focus();
}
}
}
</script>
{% endblock %}