commit a7b5e4f6412b78e2903f3e85251ee40621ddf8e2 Author: McElwain Date: Tue Jun 9 23:41:16 2026 -0500 Add JSON presets and CSV data import/export diff --git a/app/routes/doc_generator.py b/app/routes/doc_generator.py new file mode 100644 index 0000000..9c72992 --- /dev/null +++ b/app/routes/doc_generator.py @@ -0,0 +1,189 @@ +import csv +import json +import re +from pathlib import Path + +from fastapi import APIRouter, File, HTTPException, UploadFile +from fastapi.responses import FileResponse +from pydantic import BaseModel + +from tools.doc_generator.logic.document_types import get_document_type, list_document_types +from tools.doc_generator.logic.renderer import generate_docx + +router = APIRouter() + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +EXPORTS_DIR = PROJECT_ROOT / "exports" +INPUTS_DIR = PROJECT_ROOT / "inputs" +UPLOADS_DIR = INPUTS_DIR / "uploads" +JSON_UPLOADS_DIR = UPLOADS_DIR / "json" +DATA_UPLOADS_DIR = UPLOADS_DIR / "data" + + +class GenerateDocRequest(BaseModel): + document_type_id: str + data: dict + + +def safe_filename(filename: str) -> str: + filename = Path(filename or "upload").name + filename = re.sub(r"[^A-Za-z0-9._ -]+", "", filename) + filename = re.sub(r"\s+", "_", filename).strip("._ ") + return filename or "upload" + + +def save_upload(upload: UploadFile, directory: Path) -> Path: + directory.mkdir(parents=True, exist_ok=True) + filename = safe_filename(upload.filename) + path = directory / filename + + counter = 1 + while path.exists(): + stem = path.stem + suffix = path.suffix + path = directory / f"{stem}_{counter}{suffix}" + counter += 1 + + with path.open("wb") as f: + f.write(upload.file.read()) + + return path + + +@router.get("/document-types") +def document_types(): + return {"document_types": list_document_types()} + + +@router.get("/document-types/{document_type_id}") +def document_type(document_type_id: str): + try: + return get_document_type(document_type_id) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) + + +@router.post("/upload-json") +async def upload_json(file: UploadFile = File(...)): + if not file.filename.lower().endswith(".json"): + raise HTTPException(status_code=400, detail="Upload must be a JSON file.") + + path = save_upload(file, JSON_UPLOADS_DIR) + + try: + data = json.loads(path.read_text(encoding="utf-8")) + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}") + + return { + "ok": True, + "filename": path.name, + "saved_path": str(path.relative_to(PROJECT_ROOT)), + "json": data, + } + + +@router.get("/uploaded-json") +def uploaded_json_files(): + JSON_UPLOADS_DIR.mkdir(parents=True, exist_ok=True) + + files = [] + for path in sorted(JSON_UPLOADS_DIR.glob("*.json")): + files.append({ + "filename": path.name, + "saved_path": str(path.relative_to(PROJECT_ROOT)), + "modified": path.stat().st_mtime, + }) + + return {"files": files} + + +@router.get("/uploaded-json/{filename}") +def get_uploaded_json(filename: str): + file_path = JSON_UPLOADS_DIR / safe_filename(filename) + + if not file_path.exists(): + raise HTTPException(status_code=404, detail="Uploaded JSON not found.") + + try: + data = json.loads(file_path.read_text(encoding="utf-8")) + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}") + + return { + "ok": True, + "filename": file_path.name, + "saved_path": str(file_path.relative_to(PROJECT_ROOT)), + "json": data, + } + + +@router.delete("/uploaded-json/{filename}") +def delete_uploaded_json(filename: str): + file_path = JSON_UPLOADS_DIR / safe_filename(filename) + + if not file_path.exists(): + raise HTTPException(status_code=404, detail="Uploaded JSON not found.") + + file_path.unlink() + + return { + "ok": True, + "deleted": file_path.name, + } + + +@router.post("/upload-data") +async def upload_data(file: UploadFile = File(...)): + if not file.filename.lower().endswith(".csv"): + raise HTTPException(status_code=400, detail="For now, upload data as CSV.") + + path = save_upload(file, DATA_UPLOADS_DIR) + + try: + with path.open("r", encoding="utf-8-sig", newline="") as f: + reader = csv.DictReader(f) + rows = list(reader) + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Could not read CSV: {exc}") + + if not rows: + raise HTTPException(status_code=400, detail="CSV has no data rows.") + + return { + "ok": True, + "filename": path.name, + "saved_path": str(path.relative_to(PROJECT_ROOT)), + "data": rows[0], + "row_count": len(rows), + } + + +@router.post("/generate") +def generate_document(request: GenerateDocRequest): + try: + output_path = generate_docx(request.document_type_id, request.data) + except FileNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) + except Exception as exc: + raise HTTPException(status_code=400, detail=str(exc)) + + return { + "ok": True, + "filename": output_path.name, + "download_url": f"/api/doc-generator/download/{output_path.name}" + } + + +@router.get("/download/{filename}") +def download(filename: str): + file_path = EXPORTS_DIR / safe_filename(filename) + + if not file_path.exists(): + raise HTTPException(status_code=404, detail="File not found") + + return FileResponse( + path=file_path, + filename=file_path.name, + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..9b29b18 --- /dev/null +++ b/static/app.js @@ -0,0 +1,613 @@ +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 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 = ""; + +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 = 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 renderDocumentType(documentType) { + fieldsContainer.innerHTML = ""; + currentFields = []; + + 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(); + + if (defaultDocumentTypes.length > 0) { + await loadDefaultDocumentType(defaultDocumentTypes[0].id); + } + + await loadUploadedJsonOptions(); +} + +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(); +} + +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(); + 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 = ""; + } + setStatus(""); +}); + +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, + 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(); + } +}); diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..c514093 --- /dev/null +++ b/static/index.html @@ -0,0 +1,67 @@ + + + + Document Generator + + + +
+
+

Document Generator

+
+ + + + + + + + + + + +

Document Type

+ + + + + +
+ +
+
+ +

Generate Documents

+ +
+
+ +
+
+
+ +
+
+ + + + diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..420de67 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,303 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f0f0f0; +} + +.container { + max-width: 800px; + margin: 20px auto; + padding: 20px; + background-color: #fff; + border-radius: 8px; + box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); +} + +.heading { + text-align: center; + margin-bottom: 20px; +} + +.heading h1 { + font-size: 24px; + margin: 0; + color: #333; +} + +h2 { + font-size: 20px; + margin: 20px 0 10px; + color: #555; +} + +h3 { + font-size: 18px; + margin: 10px 0 5px; + color: #666; +} + +.form-row { + display: flex; + flex-wrap: wrap; + margin-bottom: 15px; + justify-content: space-between; +} + +.form-group { + flex-basis: calc(33.33% - 10px); + padding: 0 10px; + box-sizing: border-box; + margin: 5px; +} + +.form-group label { + font-weight: bold; + display: block; + margin-bottom: 5px; + color: #333; + font-size: 14px; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; + font-size: 14px; + font-family: Arial, sans-serif; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + border-color: #007bff; + outline: none; +} + +textarea { + height: 100px; + resize: vertical; +} + +button, +.file-button, +input[type="submit"] { + display: inline-block; + padding: 5px 10px; + margin: 4px; + border: 1px solid #aaa; + border-radius: 3px; + background-color: #e9e9ef; + color: #222; + font-size: 14px; + font-family: Arial, sans-serif; + cursor: pointer; +} + +.file-button input { + display: none; +} + +#documentDescription { + color: #666; + margin: 10px 5px; +} + +#status { + margin-top: 20px; + padding: 12px; + min-height: 32px; + background-color: #eee; + word-break: break-word; +} + +#status a { + font-weight: bold; + color: #0645ad; +} + +.json-section { + margin-top: 10px; +} + +.json-section details { + margin: 10px 0; +} + +.json-section summary { + font-size: 18px; + margin: 10px 0 8px; + color: #666; + font-weight: bold; + cursor: pointer; +} + +.json-section h2 { + font-size: 20px; + margin: 20px 0 10px; + color: #555; +} + +.json-section h3, +.json-section h4 { + font-size: 18px; + margin: 10px 0 5px; + color: #666; +} + +.document-picker { + margin: 10px 0 18px; + padding: 12px; + background: #f7f7f7; + border: 1px solid #ddd; + border-radius: 6px; +} + +.document-picker h3 { + margin: 8px 0 6px; + color: #555; +} + +.picker-list { + margin-bottom: 10px; +} + +.picker-item { + display: block; + width: 100%; + text-align: left; + margin: 4px 0; + padding: 7px 10px; +} + +.picker-item.active { + border-color: #6b35ff; + outline: 2px solid #6b35ff; + background: #f1edff; +} + +.uploaded-json-row { + display: flex; + gap: 6px; + align-items: center; +} + +.uploaded-json-load { + flex: 1; +} + +.delete-json-button { + flex: 0 0 34px; + width: 34px; + text-align: center; + color: #8a0000; + font-weight: bold; +} + +.picker-empty { + color: #777; + font-style: italic; + padding: 4px 0 8px; +} + +.dropdown-picker { + position: relative; + margin: 10px 0 18px; + padding: 0; + background: transparent; + border: none; +} + +.document-picker-toggle { + display: flex; + justify-content: space-between; + align-items: center; + width: 300px; + text-align: left; + padding: 8px 10px; + margin: 4px 0; + border: 1px solid #999; + border-radius: 4px; + background: #e9e9ef; + font-size: 14px; +} + +.document-picker-toggle.open { + border-color: #6b35ff; + outline: 2px solid #6b35ff; +} + +.dropdown-arrow { + margin-left: 12px; + font-size: 12px; +} + +.document-picker-menu { + display: none; + position: absolute; + z-index: 1000; + width: 420px; + max-height: 420px; + overflow-y: auto; + margin-top: 4px; + padding: 12px; + background: #f7f7f7; + border: 1px solid #bbb; + border-radius: 6px; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2); +} + +.document-picker-menu.open { + display: block; +} + +.document-picker-menu h3 { + margin: 8px 0 6px; + color: #555; +} + +.picker-list { + margin-bottom: 10px; +} + +.picker-item { + display: block; + width: 100%; + text-align: left; + margin: 4px 0; + padding: 7px 10px; +} + +.picker-item.active { + border-color: #6b35ff; + outline: 2px solid #6b35ff; + background: #f1edff; +} + +.uploaded-json-row { + display: flex; + gap: 6px; + align-items: center; +} + +.uploaded-json-load { + flex: 1; +} + +.delete-json-button { + flex: 0 0 34px; + width: 34px; + text-align: center; + color: #8a0000; + font-weight: bold; +} + +.picker-empty { + color: #777; + font-style: italic; + padding: 4px 0 8px; +} diff --git a/tools/doc_generator/content/document_types/blank_merge.json b/tools/doc_generator/content/document_types/blank_merge.json new file mode 100644 index 0000000..069060f --- /dev/null +++ b/tools/doc_generator/content/document_types/blank_merge.json @@ -0,0 +1,637 @@ +{ + "id": "blank_merge", + "name": "Blank Merge Form", + "description": "Generic merge form with 100 placeholder fields.", + "template": "blank_merge.docx", + "outputFilename": "blank_merge_{timestamp_YYYY-MM-DD_HH-mm-ss}.docx", + "sections": [ + { + "heading": "Fields 1-25", + "collapsible": false, + "defaultOpen": true, + "fields": [ + { + "name": "field1", + "label": "Field 1", + "type": "text", + "required": false + }, + { + "name": "field2", + "label": "Field 2", + "type": "text", + "required": false + }, + { + "name": "field3", + "label": "Field 3", + "type": "text", + "required": false + }, + { + "name": "field4", + "label": "Field 4", + "type": "text", + "required": false + }, + { + "name": "field5", + "label": "Field 5", + "type": "text", + "required": false + }, + { + "name": "field6", + "label": "Field 6", + "type": "text", + "required": false + }, + { + "name": "field7", + "label": "Field 7", + "type": "text", + "required": false + }, + { + "name": "field8", + "label": "Field 8", + "type": "text", + "required": false + }, + { + "name": "field9", + "label": "Field 9", + "type": "text", + "required": false + }, + { + "name": "field10", + "label": "Field 10", + "type": "textarea", + "required": false + }, + { + "name": "field11", + "label": "Field 11", + "type": "text", + "required": false + }, + { + "name": "field12", + "label": "Field 12", + "type": "text", + "required": false + }, + { + "name": "field13", + "label": "Field 13", + "type": "text", + "required": false + }, + { + "name": "field14", + "label": "Field 14", + "type": "text", + "required": false + }, + { + "name": "field15", + "label": "Field 15", + "type": "text", + "required": false + }, + { + "name": "field16", + "label": "Field 16", + "type": "text", + "required": false + }, + { + "name": "field17", + "label": "Field 17", + "type": "text", + "required": false + }, + { + "name": "field18", + "label": "Field 18", + "type": "text", + "required": false + }, + { + "name": "field19", + "label": "Field 19", + "type": "text", + "required": false + }, + { + "name": "field20", + "label": "Field 20", + "type": "textarea", + "required": false + }, + { + "name": "field21", + "label": "Field 21", + "type": "text", + "required": false + }, + { + "name": "field22", + "label": "Field 22", + "type": "text", + "required": false + }, + { + "name": "field23", + "label": "Field 23", + "type": "text", + "required": false + }, + { + "name": "field24", + "label": "Field 24", + "type": "text", + "required": false + }, + { + "name": "field25", + "label": "Field 25", + "type": "text", + "required": false + } + ] + }, + { + "heading": "Fields 26-50", + "collapsible": true, + "defaultOpen": false, + "fields": [ + { + "name": "field26", + "label": "Field 26", + "type": "text", + "required": false + }, + { + "name": "field27", + "label": "Field 27", + "type": "text", + "required": false + }, + { + "name": "field28", + "label": "Field 28", + "type": "text", + "required": false + }, + { + "name": "field29", + "label": "Field 29", + "type": "text", + "required": false + }, + { + "name": "field30", + "label": "Field 30", + "type": "textarea", + "required": false + }, + { + "name": "field31", + "label": "Field 31", + "type": "text", + "required": false + }, + { + "name": "field32", + "label": "Field 32", + "type": "text", + "required": false + }, + { + "name": "field33", + "label": "Field 33", + "type": "text", + "required": false + }, + { + "name": "field34", + "label": "Field 34", + "type": "text", + "required": false + }, + { + "name": "field35", + "label": "Field 35", + "type": "text", + "required": false + }, + { + "name": "field36", + "label": "Field 36", + "type": "text", + "required": false + }, + { + "name": "field37", + "label": "Field 37", + "type": "text", + "required": false + }, + { + "name": "field38", + "label": "Field 38", + "type": "text", + "required": false + }, + { + "name": "field39", + "label": "Field 39", + "type": "text", + "required": false + }, + { + "name": "field40", + "label": "Field 40", + "type": "textarea", + "required": false + }, + { + "name": "field41", + "label": "Field 41", + "type": "text", + "required": false + }, + { + "name": "field42", + "label": "Field 42", + "type": "text", + "required": false + }, + { + "name": "field43", + "label": "Field 43", + "type": "text", + "required": false + }, + { + "name": "field44", + "label": "Field 44", + "type": "text", + "required": false + }, + { + "name": "field45", + "label": "Field 45", + "type": "text", + "required": false + }, + { + "name": "field46", + "label": "Field 46", + "type": "text", + "required": false + }, + { + "name": "field47", + "label": "Field 47", + "type": "text", + "required": false + }, + { + "name": "field48", + "label": "Field 48", + "type": "text", + "required": false + }, + { + "name": "field49", + "label": "Field 49", + "type": "text", + "required": false + }, + { + "name": "field50", + "label": "Field 50", + "type": "textarea", + "required": false + } + ] + }, + { + "heading": "Fields 51-75", + "collapsible": true, + "defaultOpen": false, + "fields": [ + { + "name": "field51", + "label": "Field 51", + "type": "text", + "required": false + }, + { + "name": "field52", + "label": "Field 52", + "type": "text", + "required": false + }, + { + "name": "field53", + "label": "Field 53", + "type": "text", + "required": false + }, + { + "name": "field54", + "label": "Field 54", + "type": "text", + "required": false + }, + { + "name": "field55", + "label": "Field 55", + "type": "text", + "required": false + }, + { + "name": "field56", + "label": "Field 56", + "type": "text", + "required": false + }, + { + "name": "field57", + "label": "Field 57", + "type": "text", + "required": false + }, + { + "name": "field58", + "label": "Field 58", + "type": "text", + "required": false + }, + { + "name": "field59", + "label": "Field 59", + "type": "text", + "required": false + }, + { + "name": "field60", + "label": "Field 60", + "type": "textarea", + "required": false + }, + { + "name": "field61", + "label": "Field 61", + "type": "text", + "required": false + }, + { + "name": "field62", + "label": "Field 62", + "type": "text", + "required": false + }, + { + "name": "field63", + "label": "Field 63", + "type": "text", + "required": false + }, + { + "name": "field64", + "label": "Field 64", + "type": "text", + "required": false + }, + { + "name": "field65", + "label": "Field 65", + "type": "text", + "required": false + }, + { + "name": "field66", + "label": "Field 66", + "type": "text", + "required": false + }, + { + "name": "field67", + "label": "Field 67", + "type": "text", + "required": false + }, + { + "name": "field68", + "label": "Field 68", + "type": "text", + "required": false + }, + { + "name": "field69", + "label": "Field 69", + "type": "text", + "required": false + }, + { + "name": "field70", + "label": "Field 70", + "type": "textarea", + "required": false + }, + { + "name": "field71", + "label": "Field 71", + "type": "text", + "required": false + }, + { + "name": "field72", + "label": "Field 72", + "type": "text", + "required": false + }, + { + "name": "field73", + "label": "Field 73", + "type": "text", + "required": false + }, + { + "name": "field74", + "label": "Field 74", + "type": "text", + "required": false + }, + { + "name": "field75", + "label": "Field 75", + "type": "text", + "required": false + } + ] + }, + { + "heading": "Fields 76-100", + "collapsible": true, + "defaultOpen": false, + "fields": [ + { + "name": "field76", + "label": "Field 76", + "type": "text", + "required": false + }, + { + "name": "field77", + "label": "Field 77", + "type": "text", + "required": false + }, + { + "name": "field78", + "label": "Field 78", + "type": "text", + "required": false + }, + { + "name": "field79", + "label": "Field 79", + "type": "text", + "required": false + }, + { + "name": "field80", + "label": "Field 80", + "type": "textarea", + "required": false + }, + { + "name": "field81", + "label": "Field 81", + "type": "text", + "required": false + }, + { + "name": "field82", + "label": "Field 82", + "type": "text", + "required": false + }, + { + "name": "field83", + "label": "Field 83", + "type": "text", + "required": false + }, + { + "name": "field84", + "label": "Field 84", + "type": "text", + "required": false + }, + { + "name": "field85", + "label": "Field 85", + "type": "text", + "required": false + }, + { + "name": "field86", + "label": "Field 86", + "type": "text", + "required": false + }, + { + "name": "field87", + "label": "Field 87", + "type": "text", + "required": false + }, + { + "name": "field88", + "label": "Field 88", + "type": "text", + "required": false + }, + { + "name": "field89", + "label": "Field 89", + "type": "text", + "required": false + }, + { + "name": "field90", + "label": "Field 90", + "type": "textarea", + "required": false + }, + { + "name": "field91", + "label": "Field 91", + "type": "text", + "required": false + }, + { + "name": "field92", + "label": "Field 92", + "type": "text", + "required": false + }, + { + "name": "field93", + "label": "Field 93", + "type": "text", + "required": false + }, + { + "name": "field94", + "label": "Field 94", + "type": "text", + "required": false + }, + { + "name": "field95", + "label": "Field 95", + "type": "text", + "required": false + }, + { + "name": "field96", + "label": "Field 96", + "type": "text", + "required": false + }, + { + "name": "field97", + "label": "Field 97", + "type": "text", + "required": false + }, + { + "name": "field98", + "label": "Field 98", + "type": "text", + "required": false + }, + { + "name": "field99", + "label": "Field 99", + "type": "text", + "required": false + }, + { + "name": "field100", + "label": "Field 100", + "type": "textarea", + "required": false + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/doc_generator/content/templates/blank_merge.docx b/tools/doc_generator/content/templates/blank_merge.docx new file mode 100644 index 0000000..782c1bb Binary files /dev/null and b/tools/doc_generator/content/templates/blank_merge.docx differ