const documentTypeSelect = document.getElementById("documentTypeSelect"); const documentDescription = document.getElementById("documentDescription"); const fieldsContainer = document.getElementById("fieldsContainer"); const docForm = document.getElementById("docForm"); const statusBox = document.getElementById("status"); const clearFormButton = document.getElementById("clearFormButton"); const presetFileInput = document.getElementById("presetFileInput"); const dataFileInput = document.getElementById("dataFileInput"); const downloadDataButton = document.getElementById("downloadDataButton"); const downloadJsonButton = document.getElementById("downloadJsonButton"); const defaultDocumentOptions = document.getElementById("defaultDocumentOptions"); const uploadedJsonOptions = document.getElementById("uploadedJsonOptions"); const legacyTemplateSelect = document.getElementById("legacyTemplateSelect"); const legacyTemplateRow = document.getElementById("legacyTemplateRow"); const documentPickerToggle = document.getElementById("documentPickerToggle"); const documentPickerMenu = document.getElementById("documentPickerMenu"); const selectedDocumentLabel = document.getElementById("selectedDocumentLabel"); let currentDocumentType = null; let currentFields = []; let defaultDocumentTypes = []; let activePickerKind = "default"; let activePickerId = ""; let selectedTemplateId = ""; function setStatus(message) { statusBox.innerHTML = message || ""; } function setActivePicker(kind, id, label = "") { activePickerKind = kind; activePickerId = id; document.querySelectorAll(".picker-item").forEach(item => { item.classList.toggle( "active", item.dataset.kind === kind && item.dataset.id === id ); }); if (label) { selectedDocumentLabel.textContent = label; } } function normalizeField(field, index, inheritedSection = "General Fields") { return { name: field.name || `field${index + 1}`, label: field.label || field.name || `Field ${index + 1}`, type: field.type || "text", required: Boolean(field.required), section: field.section || inheritedSection, value: field.value ?? "", options: field.options || [] }; } function createInput(field) { let input; if (field.type === "textarea") { input = document.createElement("textarea"); } else if (field.type === "select") { input = document.createElement("select"); for (const option of field.options || []) { const opt = document.createElement("option"); opt.value = option.value ?? option; opt.textContent = option.label ?? option; input.appendChild(opt); } } else { input = document.createElement("input"); input.type = "text"; if (field.type === "autocomplete" && field.list) { input.setAttribute("list", `datalist-${field.list}`); } else { input.type = field.type || "text"; } } input.id = field.name; input.name = field.name; input.required = field.required; input.value = field.value ?? ""; return input; } function makeFormGroup(field) { const group = document.createElement("div"); group.className = "form-group"; const label = document.createElement("label"); label.setAttribute("for", field.name); label.textContent = field.label; group.appendChild(label); group.appendChild(createInput(field)); return group; } function appendFieldRows(parent, fields) { for (let i = 0; i < fields.length; i += 3) { const row = document.createElement("div"); row.className = "form-row"; for (const field of fields.slice(i, i + 3)) { row.appendChild(makeFormGroup(field)); } parent.appendChild(row); } } function renderSection(section, level = 2) { const sectionWrapper = document.createElement("div"); sectionWrapper.className = "json-section"; const collapsible = section.collapsible !== false; const defaultOpen = Boolean(section.defaultOpen); let contentParent = sectionWrapper; if (collapsible) { const details = document.createElement("details"); details.open = defaultOpen; const summary = document.createElement("summary"); summary.textContent = section.heading || "Section"; details.appendChild(summary); sectionWrapper.appendChild(details); contentParent = details; } else { const heading = document.createElement(`h${Math.min(level, 4)}`); heading.textContent = section.heading || "Section"; sectionWrapper.appendChild(heading); } const normalizedFields = (section.fields || []).map((field, index) => normalizeField(field, currentFields.length + index, section.heading || "General Fields") ); currentFields.push(...normalizedFields); appendFieldRows(contentParent, normalizedFields); for (const subsection of section.subsections || []) { contentParent.appendChild(renderSection(subsection, level + 1)); } return sectionWrapper; } function renderFlatFields(fields) { const bySection = {}; const normalizedFields = fields.map((field, index) => normalizeField(field, index)); for (const field of normalizedFields) { const section = field.section || "General Fields"; if (!bySection[section]) bySection[section] = []; bySection[section].push(field); } currentFields = []; for (const [sectionName, sectionFields] of Object.entries(bySection)) { fieldsContainer.appendChild(renderSection({ heading: sectionName, collapsible: sectionName !== "General Fields", defaultOpen: sectionName === "General Fields", fields: sectionFields })); } } function renderDatalists(documentType) { document.querySelectorAll("datalist[data-generated-list='true']").forEach(el => el.remove()); const lists = documentType.lists || {}; for (const [listName, values] of Object.entries(lists)) { const datalist = document.createElement("datalist"); datalist.id = `datalist-${listName}`; datalist.dataset.generatedList = "true"; for (const value of values || []) { const option = document.createElement("option"); option.value = value; datalist.appendChild(option); } document.body.appendChild(datalist); } } function renderTemplateSelector(documentType) { if (!legacyTemplateSelect || !legacyTemplateRow) return; const templates = documentType.templates || []; legacyTemplateSelect.innerHTML = ""; if (!templates.length) { legacyTemplateRow.style.display = "none"; selectedTemplateId = ""; return; } legacyTemplateRow.style.display = "block"; for (const template of templates) { const option = document.createElement("option"); option.value = template.id; option.textContent = template.label || template.id; legacyTemplateSelect.appendChild(option); } const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${documentType.id}`); selectedTemplateId = savedTemplate && templates.some(t => t.id === savedTemplate) ? savedTemplate : (documentType.defaultTemplateId || templates[0].id); legacyTemplateSelect.value = selectedTemplateId; } function getSelectedTemplateId() { if (!legacyTemplateSelect || legacyTemplateRow?.style.display === "none") { return ""; } return legacyTemplateSelect.value || selectedTemplateId || ""; } function renderDocumentType(documentType) { fieldsContainer.innerHTML = ""; currentFields = []; renderDatalists(documentType); renderTemplateSelector(documentType); if (Array.isArray(documentType.sections)) { for (const section of documentType.sections) { fieldsContainer.appendChild(renderSection(section)); } return; } renderFlatFields(documentType.fields || []); } function applyDataToForm(data) { for (const field of currentFields) { const el = document.getElementById(field.name); if (el && data[field.name] !== undefined) { el.value = data[field.name]; } } } function loadPresetObject(preset) { if (preset.documentTypeId || preset.document_type_id) { documentTypeSelect.value = preset.documentTypeId || preset.document_type_id; } if (preset.sections) { currentDocumentType = { ...currentDocumentType, ...preset, id: currentDocumentType?.id || preset.documentTypeId || preset.document_type_id || "uploaded_json" }; documentDescription.textContent = currentDocumentType.description || ""; renderDocumentType(currentDocumentType); return; } if (Array.isArray(preset.fields)) { currentDocumentType = { ...currentDocumentType, ...preset, id: currentDocumentType?.id || preset.documentTypeId || preset.document_type_id || "uploaded_json", sections: [ { heading: preset.heading || preset.name || "Imported Fields", collapsible: false, defaultOpen: true, fields: preset.fields } ] }; documentDescription.textContent = currentDocumentType.description || ""; renderDocumentType(currentDocumentType); return; } if (preset.data && typeof preset.data === "object") { applyDataToForm(preset.data); return; } if (typeof preset === "object") { currentDocumentType = { ...currentDocumentType, id: currentDocumentType?.id || "uploaded_json", name: "Uploaded JSON", sections: [ { heading: "Imported Fields", collapsible: false, defaultOpen: true, fields: Object.entries(preset).map(([key, value]) => ({ name: key, label: key, type: typeof value === "string" && value.length > 80 ? "textarea" : "text", value })) } ] }; renderDocumentType(currentDocumentType); } } async function loadDocumentTypes() { const response = await fetch("/api/doc-generator/document-types"); const json = await response.json(); defaultDocumentTypes = json.document_types || []; renderDefaultDocumentOptions(); await loadUploadedJsonOptions(); const savedSelectionRaw = localStorage.getItem("utilityAppSelectedDocument"); let savedSelection = null; try { savedSelection = savedSelectionRaw ? JSON.parse(savedSelectionRaw) : null; } catch { savedSelection = null; } if (savedSelection?.kind === "uploaded" && savedSelection.id) { await loadUploadedJson(savedSelection.id); return; } if (savedSelection?.kind === "default" && savedSelection.id) { await loadDefaultDocumentType(savedSelection.id); return; } if (defaultDocumentTypes.length > 0) { await loadDefaultDocumentType(defaultDocumentTypes[0].id); } } function renderDefaultDocumentOptions() { defaultDocumentOptions.innerHTML = ""; for (const documentType of defaultDocumentTypes) { const item = document.createElement("button"); item.type = "button"; item.className = "picker-item"; item.dataset.kind = "default"; item.dataset.id = documentType.id; item.textContent = documentType.name; item.addEventListener("click", async () => { await loadDefaultDocumentType(documentType.id); }); defaultDocumentOptions.appendChild(item); } } async function loadDefaultDocumentType(documentTypeId) { const response = await fetch(`/api/doc-generator/document-types/${documentTypeId}`); currentDocumentType = await response.json(); documentTypeSelect.value = documentTypeId; documentDescription.textContent = currentDocumentType.description || ""; renderDocumentType(currentDocumentType); setActivePicker("default", documentTypeId, currentDocumentType.name || documentTypeId); closeDocumentPicker(); localStorage.setItem("utilityAppSelectedDocument", JSON.stringify({ kind: "default", id: documentTypeId, label: currentDocumentType.name || documentTypeId })); restoreSavedFormData(); } async function loadUploadedJsonOptions() { const response = await fetch("/api/doc-generator/uploaded-json"); const json = await response.json(); uploadedJsonOptions.innerHTML = ""; if (!json.files || json.files.length === 0) { const empty = document.createElement("div"); empty.className = "picker-empty"; empty.textContent = "No uploaded JSON files yet."; uploadedJsonOptions.appendChild(empty); return; } for (const file of json.files) { const row = document.createElement("div"); row.className = "uploaded-json-row"; const loadButton = document.createElement("button"); loadButton.type = "button"; loadButton.className = "picker-item uploaded-json-load"; loadButton.dataset.kind = "uploaded"; loadButton.dataset.id = file.filename; loadButton.textContent = file.filename; loadButton.addEventListener("click", async () => { await loadUploadedJson(file.filename); }); const deleteButton = document.createElement("button"); deleteButton.type = "button"; deleteButton.className = "delete-json-button"; deleteButton.textContent = "x"; deleteButton.title = `Delete ${file.filename}`; deleteButton.addEventListener("click", async event => { event.stopPropagation(); if (!confirm(`Delete uploaded JSON file?\n\n${file.filename}`)) { return; } const response = await fetch(`/api/doc-generator/uploaded-json/${encodeURIComponent(file.filename)}`, { method: "DELETE" }); const result = await response.json(); if (!response.ok) { setStatus(`Could not delete JSON: ${result.detail || "Delete failed."}`); return; } setStatus(`Deleted uploaded JSON: ${result.deleted}`); await loadUploadedJsonOptions(); if (activePickerKind === "uploaded" && activePickerId === file.filename) { if (defaultDocumentTypes.length > 0) { await loadDefaultDocumentType(defaultDocumentTypes[0].id); } } }); row.appendChild(loadButton); row.appendChild(deleteButton); uploadedJsonOptions.appendChild(row); } } async function loadUploadedJson(filename) { const response = await fetch(`/api/doc-generator/uploaded-json/${encodeURIComponent(filename)}`); const result = await response.json(); if (!response.ok) { setStatus(`Could not load JSON: ${result.detail || "Load failed."}`); return; } loadPresetObject(result.json); setActivePicker("uploaded", filename, filename); closeDocumentPicker(); localStorage.setItem("utilityAppSelectedDocument", JSON.stringify({ kind: "uploaded", id: filename, label: filename })); restoreSavedFormData(); setStatus(`Loaded uploaded JSON: ${filename}`); } function getFormData(useFieldNameForBlanks = true) { const data = {}; for (const field of currentFields) { const el = document.getElementById(field.name); const value = el ? el.value.trim() : ""; data[field.name] = useFieldNameForBlanks ? (value || field.name) : value; } return data; } function csvEscape(value) { const text = String(value ?? ""); if (/[",\n\r]/.test(text)) { return `"${text.replaceAll('"', '""')}"`; } return text; } function downloadCurrentDataCsv() { const headers = currentFields.map(field => field.name); const data = getFormData(false); const csv = [ headers.map(csvEscape).join(","), headers.map(header => csvEscape(data[header] ?? "")).join(",") ].join("\n"); const blob = new Blob([csv], {type: "text/csv;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${currentDocumentType?.id || "document"}_data.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function cloneWithCurrentValues(value) { const byName = {}; for (const field of currentFields) { const el = document.getElementById(field.name); byName[field.name] = el ? el.value : ""; } const clone = JSON.parse(JSON.stringify(value)); function patchSection(section) { for (const field of section.fields || []) { if (field.name && byName[field.name] !== undefined) { field.value = byName[field.name]; } } for (const subsection of section.subsections || []) { patchSection(subsection); } } if (Array.isArray(clone.sections)) { for (const section of clone.sections) { patchSection(section); } } else if (Array.isArray(clone.fields)) { for (const field of clone.fields) { if (field.name && byName[field.name] !== undefined) { field.value = byName[field.name]; } } } return clone; } function downloadCurrentJson() { if (!currentDocumentType) { setStatus("No document type selected."); return; } const exportJson = cloneWithCurrentValues({ ...currentDocumentType, exportedAt: new Date().toISOString() }); const text = JSON.stringify(exportJson, null, 2); const blob = new Blob([text], {type: "application/json;charset=utf-8"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${currentDocumentType.id || "document"}_preset.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } clearFormButton.addEventListener("click", () => { for (const field of currentFields) { const el = document.getElementById(field.name); if (el) el.value = ""; } localStorage.removeItem(getFormStorageKey()); setStatus("Cleared form data."); }); presetFileInput.addEventListener("change", async event => { const file = event.target.files[0]; if (!file) return; try { const formData = new FormData(); formData.append("file", file); const response = await fetch("/api/doc-generator/upload-json", { method: "POST", body: formData }); const result = await response.json(); if (!response.ok) { throw new Error(result.detail || "JSON upload failed."); } await loadUploadedJsonOptions(); loadPresetObject(result.json); setActivePicker("uploaded", result.filename, result.filename); closeDocumentPicker(); setStatus(`Uploaded JSON: ${result.filename}
Saved to: ${result.saved_path}`); } catch (error) { setStatus(`Could not upload JSON: ${error.message}`); } finally { presetFileInput.value = ""; } }); dataFileInput.addEventListener("change", async event => { const file = event.target.files[0]; if (!file) return; try { const formData = new FormData(); formData.append("file", file); const response = await fetch("/api/doc-generator/upload-data", { method: "POST", body: formData }); const result = await response.json(); if (!response.ok) { throw new Error(result.detail || "CSV upload failed."); } applyDataToForm(result.data); setStatus(`Uploaded data CSV: ${result.filename}
Saved to: ${result.saved_path}
Rows found: ${result.row_count}`); } catch (error) { setStatus(`Could not upload CSV: ${error.message}`); } finally { dataFileInput.value = ""; } }); downloadDataButton.addEventListener("click", () => { downloadCurrentDataCsv(); }); downloadJsonButton.addEventListener("click", () => { downloadCurrentJson(); }); docForm.addEventListener("submit", async event => { event.preventDefault(); setStatus("Generating DOCX..."); const response = await fetch("/api/doc-generator/generate", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ document_type_id: currentDocumentType.id, template_id: getSelectedTemplateId(), data: getFormData(true) }) }); const json = await response.json(); if (!response.ok) { setStatus(`Error: ${json.detail || "Generation failed."}`); return; } setStatus(`Download ${json.filename}`); }); loadDocumentTypes().catch(error => { setStatus(`Startup error: ${error.message}`); }); function openDocumentPicker() { documentPickerMenu.classList.add("open"); documentPickerToggle.classList.add("open"); } function closeDocumentPicker() { documentPickerMenu.classList.remove("open"); documentPickerToggle.classList.remove("open"); } function toggleDocumentPicker() { if (documentPickerMenu.classList.contains("open")) { closeDocumentPicker(); } else { openDocumentPicker(); } } documentPickerToggle.addEventListener("click", event => { event.stopPropagation(); toggleDocumentPicker(); }); document.addEventListener("click", event => { if (!event.target.closest(".dropdown-picker")) { closeDocumentPicker(); } }); function showView(viewId) { document.querySelectorAll(".app-view").forEach(view => { view.classList.toggle("active", view.id === viewId); }); document.querySelectorAll(".nav-button").forEach(button => { button.classList.toggle("active", button.dataset.view === viewId); }); localStorage.setItem("utilityAppActiveView", viewId); } document.querySelectorAll(".nav-button").forEach(button => { button.addEventListener("click", () => { showView(button.dataset.view); }); }); const mainPageContent = document.getElementById("mainPageContent"); const saveMainPageButton = document.getElementById("saveMainPageButton"); const resetMainPageButton = document.getElementById("resetMainPageButton"); function loadMainPageContent() { const saved = localStorage.getItem("utilityAppMainPageContent"); if (saved) { mainPageContent.innerHTML = saved; } } saveMainPageButton.addEventListener("click", () => { localStorage.setItem("utilityAppMainPageContent", mainPageContent.innerHTML); setStatus("Saved main page text in this browser."); }); resetMainPageButton.addEventListener("click", () => { localStorage.removeItem("utilityAppMainPageContent"); mainPageContent.innerHTML = `

Select a tool above. This main page text is editable in the browser for quick notes, instructions, or workflow reminders.

Current tools: Document Generator and Document Processor.

`; setStatus("Reset main page text."); }); loadMainPageContent(); function getFormStorageKey() { const selectedRaw = localStorage.getItem("utilityAppSelectedDocument"); let selected = null; try { selected = selectedRaw ? JSON.parse(selectedRaw) : null; } catch { selected = null; } const kind = selected?.kind || activePickerKind || "default"; const id = selected?.id || activePickerId || currentDocumentType?.id || "unknown"; return `utilityAppFormData:${kind}:${id}`; } function saveCurrentFormData() { if (!currentFields || currentFields.length === 0) return; const data = getFormData(false); localStorage.setItem(getFormStorageKey(), JSON.stringify(data)); } function restoreSavedFormData() { const saved = localStorage.getItem(getFormStorageKey()); if (!saved) return; try { const data = JSON.parse(saved); applyDataToForm(data); } catch { return; } } fieldsContainer.addEventListener("input", () => { saveCurrentFormData(); }); fieldsContainer.addEventListener("change", () => { saveCurrentFormData(); }); window.addEventListener("beforeunload", () => { saveCurrentFormData(); }); const savedActiveView = localStorage.getItem("utilityAppActiveView"); if (savedActiveView) { showView(savedActiveView); } requestAnimationFrame(() => { document.body.classList.remove("app-loading"); }); if (legacyTemplateSelect) { legacyTemplateSelect.addEventListener("change", () => { selectedTemplateId = legacyTemplateSelect.value; if (currentDocumentType?.id) { localStorage.setItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`, selectedTemplateId); } saveCurrentFormData(); }); } function removeDynamicFields(groupId) { document.querySelectorAll(`[data-dynamic-group="${groupId}"]`).forEach(el => el.remove()); currentFields = currentFields.filter(field => field.dynamicGroup !== groupId); } function renderDynamicFieldGroup(group) { if (!currentDocumentType) return; const countEl = document.getElementById(group.countField); if (!countEl) return; const count = Math.min( parseInt(countEl.value || "0", 10) || 0, group.maxCount || 100 ); removeDynamicFields(group.id); if (count <= 0) return; const wrapper = document.createElement("div"); wrapper.dataset.dynamicGroup = group.id; wrapper.className = "json-section dynamic-field-group"; const details = document.createElement("details"); details.open = group.defaultOpen !== false; const summary = document.createElement("summary"); summary.textContent = `${group.section || "Dynamic Fields"} (${count})`; details.appendChild(summary); const generatedFields = []; for (let n = 1; n <= count; n++) { for (const fieldDef of group.fields || []) { generatedFields.push({ name: fieldDef.namePattern.replaceAll("{n}", String(n)), label: fieldDef.labelPattern.replaceAll("{n}", String(n)), type: fieldDef.type || "text", list: fieldDef.list, required: Boolean(fieldDef.required), dynamicGroup: group.id }); } } currentFields.push(...generatedFields); appendFieldRows(details, generatedFields); wrapper.appendChild(details); const countField = document.getElementById(group.countField); const sectionWrapper = countField?.closest(".json-section"); if (sectionWrapper) { sectionWrapper.appendChild(wrapper); } else { fieldsContainer.appendChild(wrapper); } restoreSavedFormData(); } function renderAllDynamicFieldGroups() { if (!currentDocumentType?.dynamicFieldGroups) return; for (const group of currentDocumentType.dynamicFieldGroups) { renderDynamicFieldGroup(group); const countEl = document.getElementById(group.countField); if (countEl && !countEl.dataset.dynamicListenerAttached) { countEl.dataset.dynamicListenerAttached = "true"; countEl.addEventListener("input", () => { saveCurrentFormData(); renderDynamicFieldGroup(group); }); countEl.addEventListener("change", () => { saveCurrentFormData(); renderDynamicFieldGroup(group); }); } } } const originalRenderDocumentTypeForDynamicGroups = renderDocumentType; renderDocumentType = function(documentType) { originalRenderDocumentTypeForDynamicGroups(documentType); renderAllDynamicFieldGroups(); loadExcelMaps(); }; const templateFileInput = document.getElementById("templateFileInput"); async function fetchUploadedTemplatesForCurrentProfile() { if (!currentDocumentType?.id) return []; try { const response = await fetch(`/api/doc-generator/uploaded-templates/${encodeURIComponent(currentDocumentType.id)}`); const json = await response.json(); if (!response.ok) { return []; } return json.templates || []; } catch { return []; } } const originalRenderTemplateSelectorWithUploads = renderTemplateSelector; renderTemplateSelector = function(documentType) { if (!legacyTemplateSelect || !legacyTemplateRow) return; const libraryTemplates = documentType.templates || []; legacyTemplateSelect.innerHTML = ""; if (!libraryTemplates.length && !documentType.id) { legacyTemplateRow.style.display = "none"; selectedTemplateId = ""; return; } legacyTemplateRow.style.display = "block"; if (libraryTemplates.length) { const libraryGroup = document.createElement("optgroup"); libraryGroup.label = "Library Templates"; for (const template of libraryTemplates) { const option = document.createElement("option"); option.value = template.id; option.textContent = template.label || template.id; libraryGroup.appendChild(option); } legacyTemplateSelect.appendChild(libraryGroup); } const uploadedGroup = document.createElement("optgroup"); uploadedGroup.label = "Uploaded Templates"; uploadedGroup.id = "uploadedTemplateOptGroup"; legacyTemplateSelect.appendChild(uploadedGroup); const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${documentType.id}`); selectedTemplateId = savedTemplate || documentType.defaultTemplateId || libraryTemplates[0]?.id || ""; if (selectedTemplateId) { legacyTemplateSelect.value = selectedTemplateId; } refreshUploadedTemplateOptions(); }; async function refreshUploadedTemplateOptions() { if (!legacyTemplateSelect || !currentDocumentType?.id) return; let uploadedGroup = document.getElementById("uploadedTemplateOptGroup"); if (!uploadedGroup) { uploadedGroup = document.createElement("optgroup"); uploadedGroup.label = "Uploaded Templates"; uploadedGroup.id = "uploadedTemplateOptGroup"; legacyTemplateSelect.appendChild(uploadedGroup); } uploadedGroup.innerHTML = ""; const uploadedTemplates = await fetchUploadedTemplatesForCurrentProfile(); for (const template of uploadedTemplates) { const option = document.createElement("option"); option.value = template.id; option.textContent = template.label || template.filename; uploadedGroup.appendChild(option); } const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`); if (savedTemplate && [...legacyTemplateSelect.options].some(option => option.value === savedTemplate)) { legacyTemplateSelect.value = savedTemplate; selectedTemplateId = savedTemplate; } } if (templateFileInput) { templateFileInput.addEventListener("change", async event => { const file = event.target.files[0]; if (!file) return; if (!currentDocumentType?.id) { setStatus("Select a profile before uploading a template."); templateFileInput.value = ""; return; } try { const formData = new FormData(); formData.append("file", file); const response = await fetch(`/api/doc-generator/upload-template/${encodeURIComponent(currentDocumentType.id)}`, { method: "POST", body: formData }); const result = await response.json(); if (!response.ok) { throw new Error(result.detail || "Template upload failed."); } await refreshUploadedTemplateOptions(); legacyTemplateSelect.value = result.template_id; selectedTemplateId = result.template_id; localStorage.setItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`, selectedTemplateId); setStatus(`Uploaded template: ${result.filename}
Saved to: ${result.saved_path}`); } catch (error) { setStatus(`Could not upload template: ${error.message}`); } finally { templateFileInput.value = ""; } }); } const excelMapSelect = document.getElementById("excelMapSelect"); const exportExcelButton = document.getElementById("exportExcelButton"); const excelImportInput = document.getElementById("excelImportInput"); async function loadExcelMaps() { if (!excelMapSelect) return; try { if (!currentDocumentType?.id) return; const response = await fetch(`/api/doc-generator/excel-maps/${encodeURIComponent(currentDocumentType.id)}`); const json = await response.json(); if (!response.ok) { throw new Error(json.detail || "Could not load Excel maps."); } excelMapSelect.innerHTML = ""; for (const item of json.maps || []) { const option = document.createElement("option"); option.value = item.id; option.textContent = `${item.label || item.id} (${item.field_count} fields)`; excelMapSelect.appendChild(option); } const saved = localStorage.getItem("utilityAppExcelMapId"); if (saved && [...excelMapSelect.options].some(option => option.value === saved)) { excelMapSelect.value = saved; } } catch (error) { setStatus(`Could not load Excel maps: ${error.message}`); } } if (excelMapSelect) { excelMapSelect.addEventListener("change", () => { localStorage.setItem("utilityAppExcelMapId", excelMapSelect.value); }); } if (exportExcelButton) { exportExcelButton.addEventListener("click", async () => { if (!currentDocumentType?.id) { setStatus("Select a profile before exporting Excel."); return; } try { saveCurrentFormData(); const response = await fetch("/api/doc-generator/export-excel", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ document_type_id: currentDocumentType.id, map_id: excelMapSelect?.value || "field_to_cell_map", data: getFormData(true) }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.detail || "Excel export failed."); } const link = document.createElement("a"); link.href = result.download_url; link.download = result.filename; document.body.appendChild(link); link.click(); link.remove(); setStatus(`Exported Excel: ${result.filename}`); } catch (error) { setStatus(`Could not export Excel: ${error.message}`); } }); } if (excelImportInput) { excelImportInput.addEventListener("change", () => { setStatus("Excel import is not wired yet. Export is ready."); excelImportInput.value = ""; }); } loadExcelMaps(); /* Excel template download button */ const downloadExcelTemplateButton = document.getElementById("downloadExcelTemplateButton"); if (downloadExcelTemplateButton) { downloadExcelTemplateButton.addEventListener("click", () => { if (!currentDocumentType?.id || !excelMapSelect?.value) { setStatus("Select a profile and Excel template/map first."); return; } window.location.href = `/api/doc-generator/excel-template-by-map/${encodeURIComponent(currentDocumentType.id)}/${encodeURIComponent(excelMapSelect.value)}`; }); } /* final legal profile UI normalization */ function legalProfileDisplayName(label) { const value = String(label || "").trim(); if (value === "Legal Profile" || value === "legal_profile") return "Legal"; return value; } function normalizeLegalUi() { const selectedLabel = document.getElementById("selectedDocumentLabel"); if (selectedLabel) { selectedLabel.textContent = legalProfileDisplayName(selectedLabel.textContent); } const description = document.getElementById("documentDescription"); if (description && currentDocumentType?.id === "legal_profile") { description.textContent = "Consumer Debt Defense Legal Profile"; description.classList.add("legal-profile-caption"); } setupSelectPicker("excelMapSelect", "Excel Template / Map"); setupSelectPicker("legacyTemplateSelect", "Document Template"); renderUploadedTemplateDeleteUi().catch(() => {}); } function setupSelectPicker(selectId, fallbackLabel) { const select = document.getElementById(selectId); if (!select) return; select.classList.add("native-select-hidden"); let picker = document.querySelector(`[data-select-picker-for="${selectId}"]`); if (!picker) { picker = document.createElement("div"); picker.className = "document-picker dropdown-picker select-picker"; picker.dataset.selectPickerFor = selectId; picker.innerHTML = `
`; select.insertAdjacentElement("afterend", picker); const toggle = picker.querySelector(".select-picker-toggle"); toggle.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); document.querySelectorAll(".select-picker.open").forEach(openPicker => { if (openPicker !== picker) openPicker.classList.remove("open"); }); picker.classList.toggle("open"); }); } const menu = picker.querySelector(".select-picker-menu"); const label = picker.querySelector(".select-picker-label"); menu.innerHTML = ""; const options = [...select.options].filter(option => option.value !== ""); if (!options.length) { label.textContent = fallbackLabel; return; } const selectedOption = select.selectedOptions?.[0] || options[0]; label.textContent = selectedOption?.textContent?.trim() || fallbackLabel; for (const option of options) { const button = document.createElement("button"); button.type = "button"; button.className = "picker-item"; if (option.value === select.value) button.classList.add("active"); button.textContent = option.textContent.trim(); button.addEventListener("click", event => { event.preventDefault(); select.value = option.value; select.dispatchEvent(new Event("change", { bubbles: true })); picker.classList.remove("open"); setupSelectPicker(selectId, fallbackLabel); }); menu.appendChild(button); } } document.addEventListener("click", () => { document.querySelectorAll(".select-picker.open").forEach(picker => picker.classList.remove("open")); }); const originalSetActivePickerForLegalUi = typeof setActivePicker === "function" ? setActivePicker : null; if (originalSetActivePickerForLegalUi) { setActivePicker = function(kind, id, label) { originalSetActivePickerForLegalUi(kind, id, legalProfileDisplayName(label)); normalizeLegalUi(); }; } const originalRenderDocumentTypeForLegalUi = renderDocumentType; renderDocumentType = function(documentType) { originalRenderDocumentTypeForLegalUi(documentType); normalizeLegalUi(); }; const originalRenderTemplateSelectorForLegalUi = renderTemplateSelector; renderTemplateSelector = function(documentType) { originalRenderTemplateSelectorForLegalUi(documentType); normalizeLegalUi(); }; async function fetchUploadedTemplatesForDeleteUi() { if (!currentDocumentType?.id) return []; const response = await fetch(`/api/doc-generator/uploaded-templates/${encodeURIComponent(currentDocumentType.id)}`); if (!response.ok) return []; const json = await response.json(); return json.templates || []; } function ensureUploadedTemplateManager() { const row = document.getElementById("legacyTemplateRow"); if (!row) return null; let manager = document.getElementById("uploadedTemplatesManager"); if (manager) return manager; manager = document.createElement("div"); manager.id = "uploadedTemplatesManager"; manager.className = "uploaded-template-manager"; manager.innerHTML = `
Uploaded Templates
`; row.appendChild(manager); return manager; } async function renderUploadedTemplateDeleteUi() { const manager = ensureUploadedTemplateManager(); if (!manager) return; const list = document.getElementById("uploadedTemplatesList"); if (!list) return; const templates = await fetchUploadedTemplatesForDeleteUi(); const uploaded = templates.filter(item => { const id = item.id || item.filename || ""; return id.startsWith("uploaded:") || item.source === "uploaded" || item.uploaded === true; }); if (!uploaded.length) { manager.style.display = "none"; list.innerHTML = ""; return; } manager.style.display = ""; list.innerHTML = ""; for (const template of uploaded) { const id = template.id || template.filename; const name = template.label || template.filename || id; const row = document.createElement("div"); row.className = "uploaded-template-row"; const nameSpan = document.createElement("span"); nameSpan.textContent = name; const deleteButton = document.createElement("button"); deleteButton.type = "button"; deleteButton.className = "delete-json-button"; deleteButton.textContent = "x"; deleteButton.title = `Delete ${name}`; deleteButton.addEventListener("click", async event => { event.preventDefault(); event.stopPropagation(); if (!confirm(`Delete uploaded template?\n\n${name}`)) return; const deleteId = String(id).replace(/^uploaded:/, ""); const response = await fetch(`/api/doc-generator/uploaded-template/${encodeURIComponent(currentDocumentType.id)}/${encodeURIComponent(deleteId)}`, { method: "DELETE" }); if (!response.ok) { setStatus("Unable to delete uploaded template."); return; } setStatus("Uploaded template deleted."); await refreshUploadedTemplatesForCurrentProfile(); normalizeLegalUi(); }); row.appendChild(nameSpan); row.appendChild(deleteButton); list.appendChild(row); } } setInterval(() => { normalizeLegalUi(); }, 1000); normalizeLegalUi();