1770 lines
88 KiB
HTML
1770 lines
88 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 id="uploadProgressList" class="space-y-2 mt-3"></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();
|
|
|
|
// 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(() => {
|
|
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 (_) {}
|
|
}
|
|
}
|
|
|
|
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>';
|
|
} catch (_) {}
|
|
}
|
|
|
|
// Authorization and JSON headers are injected by window.http.wrappedFetch
|
|
|
|
async function loadTemplates() {
|
|
try {
|
|
console.log('🔍 DEBUG: loadTemplates() called');
|
|
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');
|
|
console.log('🔍 DEBUG: Token exists:', !!token, 'Length:', token?.length);
|
|
if (!token) {
|
|
console.log('🔍 DEBUG: No token found, redirecting to login');
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
|
|
console.log('🔍 DEBUG: Making API call to:', url);
|
|
|
|
const response = await window.http.wrappedFetch(url);
|
|
console.log('🔍 DEBUG: Response status:', response.status);
|
|
|
|
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();
|
|
console.log('🔍 DEBUG: Templates loaded:', templates.length, 'items');
|
|
displayTemplates(templates);
|
|
} catch (error) {
|
|
console.error('🔍 DEBUG: 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>
|
|
<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"><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">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 %} |