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