Add JSON presets and CSV data import/export
This commit is contained in:
commit
a7b5e4f641
|
|
@ -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",
|
||||||
|
)
|
||||||
|
|
@ -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}<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,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Document Generator</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css?v=picker1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="heading">
|
||||||
|
<h1>Document Generator</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="clearFormButton" type="button">Clear Form</button>
|
||||||
|
|
||||||
|
<label class="file-button">
|
||||||
|
Upload JSON
|
||||||
|
<input type="file" id="presetFileInput" accept=".json,application/json">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button id="downloadJsonButton" type="button">Download JSON</button>
|
||||||
|
|
||||||
|
<label class="file-button">
|
||||||
|
Upload Data CSV
|
||||||
|
<input type="file" id="dataFileInput" accept=".csv,text/csv">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button id="downloadDataButton" type="button">Download Data CSV</button>
|
||||||
|
|
||||||
|
<h2>Document Type</h2>
|
||||||
|
|
||||||
|
<input type="hidden" id="documentTypeSelect">
|
||||||
|
|
||||||
|
<div class="document-picker dropdown-picker">
|
||||||
|
<button id="documentPickerToggle" class="document-picker-toggle" type="button">
|
||||||
|
<span id="selectedDocumentLabel">Select Document Type</span>
|
||||||
|
<span class="dropdown-arrow">▼</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="documentPickerMenu" class="document-picker-menu">
|
||||||
|
<h3>Default Templates</h3>
|
||||||
|
<div id="defaultDocumentOptions" class="picker-list"></div>
|
||||||
|
|
||||||
|
<h3>Uploaded JSONs</h3>
|
||||||
|
<div id="uploadedJsonOptions" class="picker-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="documentDescription"></div>
|
||||||
|
|
||||||
|
<form id="docForm">
|
||||||
|
<div id="fieldsContainer"></div>
|
||||||
|
|
||||||
|
<h2>Generate Documents</h2>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Generate DOCX">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/app.js?v=picker1"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Loading…
Reference in New Issue