1311 lines
37 KiB
JavaScript
1311 lines
37 KiB
JavaScript
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}<br>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}<br>Saved to: ${result.saved_path}<br>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(`<a href="${json.download_url}">Download ${json.filename}</a>`);
|
|
});
|
|
|
|
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 = `
|
|
<p>Select a tool above. This main page text is editable in the browser for quick notes, instructions, or workflow reminders.</p>
|
|
<p><strong>Current tools:</strong> Document Generator and Document Processor.</p>
|
|
`;
|
|
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}<br>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 normalizeLegalUi() {
|
|
const profileSelect = document.getElementById("documentTypeSelect");
|
|
const excelSelect = document.getElementById("excelMapSelect");
|
|
const templateSelect = document.getElementById("legacyTemplateSelect");
|
|
|
|
for (const select of [profileSelect, excelSelect, templateSelect]) {
|
|
if (select) {
|
|
select.classList.add("same-dropdown-style");
|
|
}
|
|
}
|
|
|
|
if (profileSelect) {
|
|
for (const option of [...profileSelect.options]) {
|
|
if (option.value === "legal_profile" || option.textContent.trim() === "Legal Profile") {
|
|
option.textContent = "Legal";
|
|
}
|
|
}
|
|
}
|
|
|
|
const longText = "Consumer debt defense legal profile based on the legacy app form fields. Additional template fields are calculated at generation time.";
|
|
for (const el of [...document.querySelectorAll("p, div, span")]) {
|
|
const value = el.textContent.trim();
|
|
if (value === longText || value === "Consumer Debt Defense Legal Profile") {
|
|
el.textContent = "Consumer Debt Defense Legal Profile";
|
|
el.classList.add("legal-profile-caption");
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<div class="uploaded-template-manager-title">Uploaded Templates</div>
|
|
<div id="uploadedTemplatesList"></div>
|
|
`;
|
|
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 item of uploaded) {
|
|
const rawId = item.id || item.filename || "";
|
|
const filename = rawId.replace(/^uploaded:/, "");
|
|
|
|
const row = document.createElement("div");
|
|
row.className = "uploaded-template-row";
|
|
|
|
const name = document.createElement("span");
|
|
name.className = "uploaded-template-name";
|
|
name.textContent = item.label || filename;
|
|
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "small-delete-button";
|
|
button.textContent = "Delete";
|
|
|
|
button.addEventListener("click", async () => {
|
|
if (!confirm(`Delete uploaded template "${filename}"?`)) return;
|
|
|
|
const response = await fetch(
|
|
`/api/doc-generator/uploaded-template/${encodeURIComponent(currentDocumentType.id)}/${encodeURIComponent(filename)}`,
|
|
{method: "DELETE"}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const json = await response.json().catch(() => ({}));
|
|
setStatus(`Could not delete template: ${json.detail || response.statusText}`);
|
|
return;
|
|
}
|
|
|
|
setStatus("Uploaded template deleted.");
|
|
|
|
if (typeof renderTemplateSelector === "function") {
|
|
await renderTemplateSelector(currentDocumentType);
|
|
}
|
|
|
|
await renderUploadedTemplateDeleteUi();
|
|
});
|
|
|
|
row.appendChild(name);
|
|
row.appendChild(button);
|
|
list.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function runLegalUiCleanup() {
|
|
normalizeLegalUi();
|
|
renderUploadedTemplateDeleteUi();
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
runLegalUiCleanup();
|
|
setTimeout(runLegalUiCleanup, 300);
|
|
setTimeout(runLegalUiCleanup, 1000);
|
|
});
|
|
|
|
const finalLegalUiObserver = new MutationObserver(() => {
|
|
clearTimeout(window.__legalUiTimer);
|
|
window.__legalUiTimer = setTimeout(runLegalUiCleanup, 100);
|
|
});
|
|
|
|
finalLegalUiObserver.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|