WIP legal profile Excel templates and UI updates
This commit is contained in:
parent
d30d620ae4
commit
1a877714e9
|
|
@ -9,6 +9,7 @@ from pydantic import BaseModel
|
||||||
|
|
||||||
from tools.doc_generator.logic.document_types import get_document_type, list_document_types
|
from tools.doc_generator.logic.document_types import get_document_type, list_document_types
|
||||||
from tools.doc_generator.logic.renderer import generate_docx
|
from tools.doc_generator.logic.renderer import generate_docx
|
||||||
|
from tools.doc_generator.logic.excel_mapper import export_profile_excel, load_excel_maps, load_excel_map, resolve_excel_template
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -18,11 +19,19 @@ INPUTS_DIR = PROJECT_ROOT / "inputs"
|
||||||
UPLOADS_DIR = INPUTS_DIR / "uploads"
|
UPLOADS_DIR = INPUTS_DIR / "uploads"
|
||||||
JSON_UPLOADS_DIR = UPLOADS_DIR / "json"
|
JSON_UPLOADS_DIR = UPLOADS_DIR / "json"
|
||||||
DATA_UPLOADS_DIR = UPLOADS_DIR / "data"
|
DATA_UPLOADS_DIR = UPLOADS_DIR / "data"
|
||||||
|
TEMPLATE_UPLOADS_DIR = UPLOADS_DIR / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
class ExportExcelRequest(BaseModel):
|
||||||
|
document_type_id: str
|
||||||
|
data: dict
|
||||||
|
map_id: str = "legacy_datafile"
|
||||||
|
|
||||||
|
|
||||||
class GenerateDocRequest(BaseModel):
|
class GenerateDocRequest(BaseModel):
|
||||||
document_type_id: str
|
document_type_id: str
|
||||||
data: dict
|
data: dict
|
||||||
|
template_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def safe_filename(filename: str) -> str:
|
def safe_filename(filename: str) -> str:
|
||||||
|
|
@ -159,10 +168,106 @@ async def upload_data(file: UploadFile = File(...)):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload-template/{document_type_id}")
|
||||||
|
async def upload_template(document_type_id: str, file: UploadFile = File(...)):
|
||||||
|
if not file.filename.lower().endswith(".docx"):
|
||||||
|
raise HTTPException(status_code=400, detail="Upload must be a DOCX template.")
|
||||||
|
|
||||||
|
# Validate profile exists.
|
||||||
|
try:
|
||||||
|
get_document_type(document_type_id)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc))
|
||||||
|
|
||||||
|
profile_dir = TEMPLATE_UPLOADS_DIR / safe_filename(document_type_id)
|
||||||
|
path = save_upload(file, profile_dir)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"filename": path.name,
|
||||||
|
"template_id": f"uploaded:{path.name}",
|
||||||
|
"saved_path": str(path.relative_to(PROJECT_ROOT)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/uploaded-templates/{document_type_id}")
|
||||||
|
def uploaded_templates(document_type_id: str):
|
||||||
|
profile_dir = TEMPLATE_UPLOADS_DIR / safe_filename(document_type_id)
|
||||||
|
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
templates = []
|
||||||
|
for path in sorted(profile_dir.glob("*.docx")):
|
||||||
|
templates.append({
|
||||||
|
"id": f"uploaded:{path.name}",
|
||||||
|
"label": path.name,
|
||||||
|
"filename": path.name,
|
||||||
|
"saved_path": str(path.relative_to(PROJECT_ROOT)),
|
||||||
|
"modified": path.stat().st_mtime,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"templates": templates}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/uploaded-template/{document_type_id}/{template_id}")
|
||||||
|
def delete_uploaded_template(document_type_id: str, template_id: str):
|
||||||
|
doc_dir = TEMPLATE_UPLOADS_DIR / safe_filename(document_type_id)
|
||||||
|
file_path = doc_dir / safe_filename(template_id)
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Template not found.")
|
||||||
|
|
||||||
|
file_path.unlink()
|
||||||
|
return {"deleted": True, "template_id": file_path.name}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/excel-maps/{document_type_id}")
|
||||||
|
def get_excel_maps_route(document_type_id: str):
|
||||||
|
try:
|
||||||
|
maps = load_excel_maps(document_type_id)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {"maps": []}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"maps": [
|
||||||
|
{
|
||||||
|
"id": item.get("id"),
|
||||||
|
"label": item.get("label", item.get("id")),
|
||||||
|
"field_count": len(item.get("field_to_cell", {})),
|
||||||
|
"fields": sorted(item.get("field_to_cell", {}).keys()),
|
||||||
|
"template": item.get("template"),
|
||||||
|
"mode": item.get("mode", "map"),
|
||||||
|
}
|
||||||
|
for item in maps
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/export-excel")
|
||||||
|
def export_excel(request: ExportExcelRequest):
|
||||||
|
try:
|
||||||
|
output_path = export_profile_excel(
|
||||||
|
request.document_type_id,
|
||||||
|
request.data,
|
||||||
|
request.map_id,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"filename": output_path.name,
|
||||||
|
"download_url": f"/api/doc-generator/download/{output_path.name}",
|
||||||
|
}
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc))
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate")
|
@router.post("/generate")
|
||||||
def generate_document(request: GenerateDocRequest):
|
def generate_document(request: GenerateDocRequest):
|
||||||
try:
|
try:
|
||||||
output_path = generate_docx(request.document_type_id, request.data)
|
output_path = generate_docx(request.document_type_id, request.data, request.template_id)
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
raise HTTPException(status_code=404, detail=str(exc))
|
raise HTTPException(status_code=404, detail=str(exc))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -175,15 +280,87 @@ def generate_document(request: GenerateDocRequest):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@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")
|
@router.get("/excel-template/{template_name}")
|
||||||
|
def download_excel_template(template_name: str):
|
||||||
|
allowed = {
|
||||||
|
"datafile": "template.xlsx",
|
||||||
|
"amortization": "amortization.xlsx",
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = allowed.get(template_name)
|
||||||
|
if not filename:
|
||||||
|
raise HTTPException(status_code=404, detail="Unknown Excel template.")
|
||||||
|
|
||||||
|
path = OLD_WORD_DOC_GENERATOR_DIR / filename
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"Excel template not found: {path}")
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=file_path,
|
path,
|
||||||
filename=file_path.name,
|
filename=filename,
|
||||||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/excel-template-by-map/{document_type_id}/{map_id}")
|
||||||
|
def download_excel_template_by_map(document_type_id: str, map_id: str):
|
||||||
|
try:
|
||||||
|
excel_map = load_excel_map(document_type_id, map_id)
|
||||||
|
template_path = resolve_excel_template(excel_map.get("template"))
|
||||||
|
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 FileResponse(
|
||||||
|
path=template_path,
|
||||||
|
filename=template_path.name,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/uploaded-template/{document_type_id}/{filename}")
|
||||||
|
def delete_uploaded_template(document_type_id: str, filename: str):
|
||||||
|
folder = TEMPLATE_UPLOADS_DIR / safe_filename(document_type_id)
|
||||||
|
path = folder / safe_filename(filename)
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Uploaded template not found.")
|
||||||
|
|
||||||
|
if path.suffix.lower() != ".docx":
|
||||||
|
raise HTTPException(status_code=400, detail="Only DOCX templates can be deleted here.")
|
||||||
|
|
||||||
|
path.unlink()
|
||||||
|
return {"deleted": True, "filename": path.name}
|
||||||
|
|
||||||
|
@router.get("/download/{filename}")
|
||||||
|
def download_file(filename: str):
|
||||||
|
safe_name = safe_filename(filename)
|
||||||
|
path = EXPORTS_DIR / safe_name
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="File not found.")
|
||||||
|
|
||||||
|
suffix = path.suffix.lower()
|
||||||
|
|
||||||
|
media_types = {
|
||||||
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
".csv": "text/csv",
|
||||||
|
".json": "application/json",
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
path,
|
||||||
|
filename=path.name,
|
||||||
|
media_type=media_types.get(suffix, "application/octet-stream"),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
560
static/app.js
560
static/app.js
|
|
@ -10,6 +10,8 @@ const downloadDataButton = document.getElementById("downloadDataButton");
|
||||||
const downloadJsonButton = document.getElementById("downloadJsonButton");
|
const downloadJsonButton = document.getElementById("downloadJsonButton");
|
||||||
const defaultDocumentOptions = document.getElementById("defaultDocumentOptions");
|
const defaultDocumentOptions = document.getElementById("defaultDocumentOptions");
|
||||||
const uploadedJsonOptions = document.getElementById("uploadedJsonOptions");
|
const uploadedJsonOptions = document.getElementById("uploadedJsonOptions");
|
||||||
|
const legacyTemplateSelect = document.getElementById("legacyTemplateSelect");
|
||||||
|
const legacyTemplateRow = document.getElementById("legacyTemplateRow");
|
||||||
const documentPickerToggle = document.getElementById("documentPickerToggle");
|
const documentPickerToggle = document.getElementById("documentPickerToggle");
|
||||||
const documentPickerMenu = document.getElementById("documentPickerMenu");
|
const documentPickerMenu = document.getElementById("documentPickerMenu");
|
||||||
const selectedDocumentLabel = document.getElementById("selectedDocumentLabel");
|
const selectedDocumentLabel = document.getElementById("selectedDocumentLabel");
|
||||||
|
|
@ -19,6 +21,7 @@ let currentFields = [];
|
||||||
let defaultDocumentTypes = [];
|
let defaultDocumentTypes = [];
|
||||||
let activePickerKind = "default";
|
let activePickerKind = "default";
|
||||||
let activePickerId = "";
|
let activePickerId = "";
|
||||||
|
let selectedTemplateId = "";
|
||||||
|
|
||||||
function setStatus(message) {
|
function setStatus(message) {
|
||||||
statusBox.innerHTML = message || "";
|
statusBox.innerHTML = message || "";
|
||||||
|
|
@ -67,7 +70,13 @@ function createInput(field) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
input = document.createElement("input");
|
input = document.createElement("input");
|
||||||
input.type = field.type || "text";
|
input.type = "text";
|
||||||
|
|
||||||
|
if (field.type === "autocomplete" && field.list) {
|
||||||
|
input.setAttribute("list", `datalist-${field.list}`);
|
||||||
|
} else {
|
||||||
|
input.type = field.type || "text";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.id = field.name;
|
input.id = field.name;
|
||||||
|
|
@ -164,9 +173,69 @@ function renderFlatFields(fields) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderDatalists(documentType) {
|
||||||
|
document.querySelectorAll("datalist[data-generated-list='true']").forEach(el => el.remove());
|
||||||
|
|
||||||
|
const lists = documentType.lists || {};
|
||||||
|
for (const [listName, values] of Object.entries(lists)) {
|
||||||
|
const datalist = document.createElement("datalist");
|
||||||
|
datalist.id = `datalist-${listName}`;
|
||||||
|
datalist.dataset.generatedList = "true";
|
||||||
|
|
||||||
|
for (const value of values || []) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = value;
|
||||||
|
datalist.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(datalist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTemplateSelector(documentType) {
|
||||||
|
if (!legacyTemplateSelect || !legacyTemplateRow) return;
|
||||||
|
|
||||||
|
const templates = documentType.templates || [];
|
||||||
|
|
||||||
|
legacyTemplateSelect.innerHTML = "";
|
||||||
|
|
||||||
|
if (!templates.length) {
|
||||||
|
legacyTemplateRow.style.display = "none";
|
||||||
|
selectedTemplateId = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyTemplateRow.style.display = "block";
|
||||||
|
|
||||||
|
for (const template of templates) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = template.id;
|
||||||
|
option.textContent = template.label || template.id;
|
||||||
|
legacyTemplateSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${documentType.id}`);
|
||||||
|
selectedTemplateId = savedTemplate && templates.some(t => t.id === savedTemplate)
|
||||||
|
? savedTemplate
|
||||||
|
: (documentType.defaultTemplateId || templates[0].id);
|
||||||
|
|
||||||
|
legacyTemplateSelect.value = selectedTemplateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedTemplateId() {
|
||||||
|
if (!legacyTemplateSelect || legacyTemplateRow?.style.display === "none") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return legacyTemplateSelect.value || selectedTemplateId || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderDocumentType(documentType) {
|
function renderDocumentType(documentType) {
|
||||||
fieldsContainer.innerHTML = "";
|
fieldsContainer.innerHTML = "";
|
||||||
currentFields = [];
|
currentFields = [];
|
||||||
|
renderDatalists(documentType);
|
||||||
|
renderTemplateSelector(documentType);
|
||||||
|
|
||||||
if (Array.isArray(documentType.sections)) {
|
if (Array.isArray(documentType.sections)) {
|
||||||
for (const section of documentType.sections) {
|
for (const section of documentType.sections) {
|
||||||
|
|
@ -600,6 +669,7 @@ docForm.addEventListener("submit", async event => {
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
document_type_id: currentDocumentType.id,
|
document_type_id: currentDocumentType.id,
|
||||||
|
template_id: getSelectedTemplateId(),
|
||||||
data: getFormData(true)
|
data: getFormData(true)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
@ -750,3 +820,491 @@ if (savedActiveView) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
document.body.classList.remove("app-loading");
|
document.body.classList.remove("app-loading");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (legacyTemplateSelect) {
|
||||||
|
legacyTemplateSelect.addEventListener("change", () => {
|
||||||
|
selectedTemplateId = legacyTemplateSelect.value;
|
||||||
|
if (currentDocumentType?.id) {
|
||||||
|
localStorage.setItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`, selectedTemplateId);
|
||||||
|
}
|
||||||
|
saveCurrentFormData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDynamicFields(groupId) {
|
||||||
|
document.querySelectorAll(`[data-dynamic-group="${groupId}"]`).forEach(el => el.remove());
|
||||||
|
currentFields = currentFields.filter(field => field.dynamicGroup !== groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDynamicFieldGroup(group) {
|
||||||
|
if (!currentDocumentType) return;
|
||||||
|
|
||||||
|
const countEl = document.getElementById(group.countField);
|
||||||
|
if (!countEl) return;
|
||||||
|
|
||||||
|
const count = Math.min(
|
||||||
|
parseInt(countEl.value || "0", 10) || 0,
|
||||||
|
group.maxCount || 100
|
||||||
|
);
|
||||||
|
|
||||||
|
removeDynamicFields(group.id);
|
||||||
|
|
||||||
|
if (count <= 0) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.dataset.dynamicGroup = group.id;
|
||||||
|
wrapper.className = "json-section dynamic-field-group";
|
||||||
|
|
||||||
|
const details = document.createElement("details");
|
||||||
|
details.open = group.defaultOpen !== false;
|
||||||
|
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
summary.textContent = `${group.section || "Dynamic Fields"} (${count})`;
|
||||||
|
details.appendChild(summary);
|
||||||
|
|
||||||
|
const generatedFields = [];
|
||||||
|
|
||||||
|
for (let n = 1; n <= count; n++) {
|
||||||
|
for (const fieldDef of group.fields || []) {
|
||||||
|
generatedFields.push({
|
||||||
|
name: fieldDef.namePattern.replaceAll("{n}", String(n)),
|
||||||
|
label: fieldDef.labelPattern.replaceAll("{n}", String(n)),
|
||||||
|
type: fieldDef.type || "text",
|
||||||
|
list: fieldDef.list,
|
||||||
|
required: Boolean(fieldDef.required),
|
||||||
|
dynamicGroup: group.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFields.push(...generatedFields);
|
||||||
|
appendFieldRows(details, generatedFields);
|
||||||
|
|
||||||
|
wrapper.appendChild(details);
|
||||||
|
|
||||||
|
const countField = document.getElementById(group.countField);
|
||||||
|
const sectionWrapper = countField?.closest(".json-section");
|
||||||
|
|
||||||
|
if (sectionWrapper) {
|
||||||
|
sectionWrapper.appendChild(wrapper);
|
||||||
|
} else {
|
||||||
|
fieldsContainer.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreSavedFormData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAllDynamicFieldGroups() {
|
||||||
|
if (!currentDocumentType?.dynamicFieldGroups) return;
|
||||||
|
|
||||||
|
for (const group of currentDocumentType.dynamicFieldGroups) {
|
||||||
|
renderDynamicFieldGroup(group);
|
||||||
|
|
||||||
|
const countEl = document.getElementById(group.countField);
|
||||||
|
if (countEl && !countEl.dataset.dynamicListenerAttached) {
|
||||||
|
countEl.dataset.dynamicListenerAttached = "true";
|
||||||
|
|
||||||
|
countEl.addEventListener("input", () => {
|
||||||
|
saveCurrentFormData();
|
||||||
|
renderDynamicFieldGroup(group);
|
||||||
|
});
|
||||||
|
|
||||||
|
countEl.addEventListener("change", () => {
|
||||||
|
saveCurrentFormData();
|
||||||
|
renderDynamicFieldGroup(group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalRenderDocumentTypeForDynamicGroups = renderDocumentType;
|
||||||
|
renderDocumentType = function(documentType) {
|
||||||
|
originalRenderDocumentTypeForDynamicGroups(documentType);
|
||||||
|
renderAllDynamicFieldGroups();
|
||||||
|
loadExcelMaps();
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateFileInput = document.getElementById("templateFileInput");
|
||||||
|
|
||||||
|
async function fetchUploadedTemplatesForCurrentProfile() {
|
||||||
|
if (!currentDocumentType?.id) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/doc-generator/uploaded-templates/${encodeURIComponent(currentDocumentType.id)}`);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.templates || [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalRenderTemplateSelectorWithUploads = renderTemplateSelector;
|
||||||
|
renderTemplateSelector = function(documentType) {
|
||||||
|
if (!legacyTemplateSelect || !legacyTemplateRow) return;
|
||||||
|
|
||||||
|
const libraryTemplates = documentType.templates || [];
|
||||||
|
|
||||||
|
legacyTemplateSelect.innerHTML = "";
|
||||||
|
|
||||||
|
if (!libraryTemplates.length && !documentType.id) {
|
||||||
|
legacyTemplateRow.style.display = "none";
|
||||||
|
selectedTemplateId = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyTemplateRow.style.display = "block";
|
||||||
|
|
||||||
|
if (libraryTemplates.length) {
|
||||||
|
const libraryGroup = document.createElement("optgroup");
|
||||||
|
libraryGroup.label = "Library Templates";
|
||||||
|
|
||||||
|
for (const template of libraryTemplates) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = template.id;
|
||||||
|
option.textContent = template.label || template.id;
|
||||||
|
libraryGroup.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyTemplateSelect.appendChild(libraryGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadedGroup = document.createElement("optgroup");
|
||||||
|
uploadedGroup.label = "Uploaded Templates";
|
||||||
|
uploadedGroup.id = "uploadedTemplateOptGroup";
|
||||||
|
legacyTemplateSelect.appendChild(uploadedGroup);
|
||||||
|
|
||||||
|
const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${documentType.id}`);
|
||||||
|
selectedTemplateId = savedTemplate || documentType.defaultTemplateId || libraryTemplates[0]?.id || "";
|
||||||
|
|
||||||
|
if (selectedTemplateId) {
|
||||||
|
legacyTemplateSelect.value = selectedTemplateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshUploadedTemplateOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
async function refreshUploadedTemplateOptions() {
|
||||||
|
if (!legacyTemplateSelect || !currentDocumentType?.id) return;
|
||||||
|
|
||||||
|
let uploadedGroup = document.getElementById("uploadedTemplateOptGroup");
|
||||||
|
|
||||||
|
if (!uploadedGroup) {
|
||||||
|
uploadedGroup = document.createElement("optgroup");
|
||||||
|
uploadedGroup.label = "Uploaded Templates";
|
||||||
|
uploadedGroup.id = "uploadedTemplateOptGroup";
|
||||||
|
legacyTemplateSelect.appendChild(uploadedGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedGroup.innerHTML = "";
|
||||||
|
|
||||||
|
const uploadedTemplates = await fetchUploadedTemplatesForCurrentProfile();
|
||||||
|
|
||||||
|
for (const template of uploadedTemplates) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = template.id;
|
||||||
|
option.textContent = template.label || template.filename;
|
||||||
|
uploadedGroup.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`);
|
||||||
|
if (savedTemplate && [...legacyTemplateSelect.options].some(option => option.value === savedTemplate)) {
|
||||||
|
legacyTemplateSelect.value = savedTemplate;
|
||||||
|
selectedTemplateId = savedTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateFileInput) {
|
||||||
|
templateFileInput.addEventListener("change", async event => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!currentDocumentType?.id) {
|
||||||
|
setStatus("Select a profile before uploading a template.");
|
||||||
|
templateFileInput.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/doc-generator/upload-template/${encodeURIComponent(currentDocumentType.id)}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.detail || "Template upload failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshUploadedTemplateOptions();
|
||||||
|
|
||||||
|
legacyTemplateSelect.value = result.template_id;
|
||||||
|
selectedTemplateId = result.template_id;
|
||||||
|
localStorage.setItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`, selectedTemplateId);
|
||||||
|
|
||||||
|
setStatus(`Uploaded template: ${result.filename}<br>Saved to: ${result.saved_path}`);
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`Could not upload template: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
templateFileInput.value = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const excelMapSelect = document.getElementById("excelMapSelect");
|
||||||
|
const exportExcelButton = document.getElementById("exportExcelButton");
|
||||||
|
const excelImportInput = document.getElementById("excelImportInput");
|
||||||
|
|
||||||
|
async function loadExcelMaps() {
|
||||||
|
if (!excelMapSelect) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!currentDocumentType?.id) return;
|
||||||
|
const response = await fetch(`/api/doc-generator/excel-maps/${encodeURIComponent(currentDocumentType.id)}`);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(json.detail || "Could not load Excel maps.");
|
||||||
|
}
|
||||||
|
|
||||||
|
excelMapSelect.innerHTML = "";
|
||||||
|
|
||||||
|
for (const item of json.maps || []) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = item.id;
|
||||||
|
option.textContent = `${item.label || item.id} (${item.field_count} fields)`;
|
||||||
|
excelMapSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = localStorage.getItem("utilityAppExcelMapId");
|
||||||
|
if (saved && [...excelMapSelect.options].some(option => option.value === saved)) {
|
||||||
|
excelMapSelect.value = saved;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`Could not load Excel maps: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excelMapSelect) {
|
||||||
|
excelMapSelect.addEventListener("change", () => {
|
||||||
|
localStorage.setItem("utilityAppExcelMapId", excelMapSelect.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportExcelButton) {
|
||||||
|
exportExcelButton.addEventListener("click", async () => {
|
||||||
|
if (!currentDocumentType?.id) {
|
||||||
|
setStatus("Select a profile before exporting Excel.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
saveCurrentFormData();
|
||||||
|
|
||||||
|
const response = await fetch("/api/doc-generator/export-excel", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
document_type_id: currentDocumentType.id,
|
||||||
|
map_id: excelMapSelect?.value || "field_to_cell_map",
|
||||||
|
data: getFormData(true)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.detail || "Excel export failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = result.download_url;
|
||||||
|
link.download = result.filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
setStatus(`Exported Excel: ${result.filename}`);
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`Could not export Excel: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excelImportInput) {
|
||||||
|
excelImportInput.addEventListener("change", () => {
|
||||||
|
setStatus("Excel import is not wired yet. Export is ready.");
|
||||||
|
excelImportInput.value = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadExcelMaps();
|
||||||
|
|
||||||
|
/* Excel template download button */
|
||||||
|
const downloadExcelTemplateButton = document.getElementById("downloadExcelTemplateButton");
|
||||||
|
|
||||||
|
if (downloadExcelTemplateButton) {
|
||||||
|
downloadExcelTemplateButton.addEventListener("click", () => {
|
||||||
|
if (!currentDocumentType?.id || !excelMapSelect?.value) {
|
||||||
|
setStatus("Select a profile and Excel template/map first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = `/api/doc-generator/excel-template-by-map/${encodeURIComponent(currentDocumentType.id)}/${encodeURIComponent(excelMapSelect.value)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* final legal profile UI normalization */
|
||||||
|
|
||||||
|
function normalizeLegalUi() {
|
||||||
|
const profileSelect = document.getElementById("documentTypeSelect");
|
||||||
|
const excelSelect = document.getElementById("excelMapSelect");
|
||||||
|
const templateSelect = document.getElementById("legacyTemplateSelect");
|
||||||
|
|
||||||
|
for (const select of [profileSelect, excelSelect, templateSelect]) {
|
||||||
|
if (select) {
|
||||||
|
select.classList.add("same-dropdown-style");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileSelect) {
|
||||||
|
for (const option of [...profileSelect.options]) {
|
||||||
|
if (option.value === "legal_profile" || option.textContent.trim() === "Legal Profile") {
|
||||||
|
option.textContent = "Legal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const longText = "Consumer debt defense legal profile based on the legacy app form fields. Additional template fields are calculated at generation time.";
|
||||||
|
for (const el of [...document.querySelectorAll("p, div, span")]) {
|
||||||
|
const value = el.textContent.trim();
|
||||||
|
if (value === longText || value === "Consumer Debt Defense Legal Profile") {
|
||||||
|
el.textContent = "Consumer Debt Defense Legal Profile";
|
||||||
|
el.classList.add("legal-profile-caption");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUploadedTemplatesForDeleteUi() {
|
||||||
|
if (!currentDocumentType?.id) return [];
|
||||||
|
const response = await fetch(`/api/doc-generator/uploaded-templates/${encodeURIComponent(currentDocumentType.id)}`);
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const json = await response.json();
|
||||||
|
return json.templates || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUploadedTemplateManager() {
|
||||||
|
const row = document.getElementById("legacyTemplateRow");
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
let manager = document.getElementById("uploadedTemplatesManager");
|
||||||
|
if (manager) return manager;
|
||||||
|
|
||||||
|
manager = document.createElement("div");
|
||||||
|
manager.id = "uploadedTemplatesManager";
|
||||||
|
manager.className = "uploaded-template-manager";
|
||||||
|
manager.innerHTML = `
|
||||||
|
<div class="uploaded-template-manager-title">Uploaded Templates</div>
|
||||||
|
<div id="uploadedTemplatesList"></div>
|
||||||
|
`;
|
||||||
|
row.appendChild(manager);
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderUploadedTemplateDeleteUi() {
|
||||||
|
const manager = ensureUploadedTemplateManager();
|
||||||
|
if (!manager) return;
|
||||||
|
|
||||||
|
const list = document.getElementById("uploadedTemplatesList");
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
const templates = await fetchUploadedTemplatesForDeleteUi();
|
||||||
|
const uploaded = templates.filter(item => {
|
||||||
|
const id = item.id || item.filename || "";
|
||||||
|
return id.startsWith("uploaded:") || item.source === "uploaded" || item.uploaded === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uploaded.length) {
|
||||||
|
manager.style.display = "none";
|
||||||
|
list.innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.style.display = "";
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
for (const item of uploaded) {
|
||||||
|
const rawId = item.id || item.filename || "";
|
||||||
|
const filename = rawId.replace(/^uploaded:/, "");
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "uploaded-template-row";
|
||||||
|
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.className = "uploaded-template-name";
|
||||||
|
name.textContent = item.label || filename;
|
||||||
|
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "small-delete-button";
|
||||||
|
button.textContent = "Delete";
|
||||||
|
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
if (!confirm(`Delete uploaded template "${filename}"?`)) return;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/doc-generator/uploaded-template/${encodeURIComponent(currentDocumentType.id)}/${encodeURIComponent(filename)}`,
|
||||||
|
{method: "DELETE"}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const json = await response.json().catch(() => ({}));
|
||||||
|
setStatus(`Could not delete template: ${json.detail || response.statusText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("Uploaded template deleted.");
|
||||||
|
|
||||||
|
if (typeof renderTemplateSelector === "function") {
|
||||||
|
await renderTemplateSelector(currentDocumentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderUploadedTemplateDeleteUi();
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(name);
|
||||||
|
row.appendChild(button);
|
||||||
|
list.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runLegalUiCleanup() {
|
||||||
|
normalizeLegalUi();
|
||||||
|
renderUploadedTemplateDeleteUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
runLegalUiCleanup();
|
||||||
|
setTimeout(runLegalUiCleanup, 300);
|
||||||
|
setTimeout(runLegalUiCleanup, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalLegalUiObserver = new MutationObserver(() => {
|
||||||
|
clearTimeout(window.__legalUiTimer);
|
||||||
|
window.__legalUiTimer = setTimeout(runLegalUiCleanup, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
finalLegalUiObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Utility App</title>
|
<title>Utility App</title>
|
||||||
<link rel="stylesheet" href="/static/styles.css?v=shell1">
|
<link rel="stylesheet" href="/static/styles.css?v=legalui3">
|
||||||
</head>
|
</head>
|
||||||
<body class="app-loading">
|
<body class="app-loading">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
|
|
@ -55,6 +55,11 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button id="downloadDataButton" class="tool-action-button" type="button">Download Data CSV</button>
|
<button id="downloadDataButton" class="tool-action-button" type="button">Download Data CSV</button>
|
||||||
|
|
||||||
|
<label class="file-button tool-action-button">
|
||||||
|
Upload Template
|
||||||
|
<input type="file" id="templateFileInput" accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Document Type</h2>
|
<h2>Document Type</h2>
|
||||||
|
|
@ -78,7 +83,44 @@
|
||||||
|
|
||||||
<div id="documentDescription"></div>
|
<div id="documentDescription"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<form id="docForm">
|
<form id="docForm">
|
||||||
|
|
||||||
|
<div id="excelTransferRow" class="excel-transfer-row">
|
||||||
|
<div class="select-block excel-map-block">
|
||||||
|
<label for="excelMapSelect">Excel Template / Map</label>
|
||||||
|
<select id="excelMapSelect" class="styled-select"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="downloadExcelTemplateButton" class="tool-action-button" type="button">Download Excel Template</button>
|
||||||
|
<button id="exportExcelButton" class="tool-action-button" type="button">Export Excel</button>
|
||||||
|
|
||||||
|
<label class="file-button tool-action-button">
|
||||||
|
Import Excel
|
||||||
|
<input type="file" id="excelImportInput" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="legacyTemplateRow" class="template-picker-row" style="display:none;">
|
||||||
|
<label for="legacyTemplateSelect">Document Template</label>
|
||||||
|
<select id="legacyTemplateSelect" class="styled-select"></select>
|
||||||
|
|
||||||
|
<div id="uploadedTemplatesManager" class="uploaded-template-manager" style="display:none;">
|
||||||
|
<div class="uploaded-template-manager-title">Uploaded Templates</div>
|
||||||
|
<div id="uploadedTemplatesList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="fieldsContainer"></div>
|
<div id="fieldsContainer"></div>
|
||||||
|
|
||||||
<h2>Generate Documents</h2>
|
<h2>Generate Documents</h2>
|
||||||
|
|
@ -110,6 +152,6 @@
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/static/app.js?v=shell1"></script>
|
<script src="/static/app.js?v=legalui3"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -429,3 +429,481 @@ body.app-loading .container {
|
||||||
.container {
|
.container {
|
||||||
transition: opacity 120ms ease-out;
|
transition: opacity 120ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-button-row {
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-action-button,
|
||||||
|
button.tool-action-button,
|
||||||
|
label.tool-action-button {
|
||||||
|
flex: 0 0 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-transfer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 12px 0 22px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-transfer-row label {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-transfer-row select {
|
||||||
|
min-width: 260px;
|
||||||
|
flex: 1 1 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-transfer-row .tool-action-button {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#exportExcelButton:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#exportExcelButton:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-hidden-select {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-transfer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 12px 0 14px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-picker-row {
|
||||||
|
margin: 0 0 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-block {
|
||||||
|
flex: 1 1 320px;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select-block label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 7px 34px 7px 10px;
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker-button::after {
|
||||||
|
content: "▾";
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 7px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 3px);
|
||||||
|
max-height: 340px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 6px 18px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker.open .custom-picker-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker-group-label {
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #555;
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker-option {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
background: #fff;
|
||||||
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
color: #111;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-picker-option:hover,
|
||||||
|
.custom-picker-option.selected {
|
||||||
|
background: #eaf2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadExcelTemplateButton,
|
||||||
|
#exportExcelButton {
|
||||||
|
flex: 0 0 145px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-transfer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 14px 0 18px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-map-block {
|
||||||
|
flex: 1 1 360px;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-picker-row {
|
||||||
|
margin: 0 0 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-picker-row label,
|
||||||
|
.excel-map-block label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid #999;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadExcelTemplateButton,
|
||||||
|
#exportExcelButton,
|
||||||
|
.excel-transfer-row .file-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-type-caption {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #8a8a8a;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excel-transfer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 12px 0 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-block {
|
||||||
|
flex: 1 1 340px;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-picker-row {
|
||||||
|
margin: 0 0 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-picker-row label,
|
||||||
|
.select-block label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-manager {
|
||||||
|
margin-top: 10px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-manager-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-name {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-delete-button {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
background: #f7f7f7;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadExcelTemplateButton,
|
||||||
|
#exportExcelButton,
|
||||||
|
.excel-transfer-row .file-button {
|
||||||
|
min-height: 38px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* unified dropdown styling */
|
||||||
|
#documentTypeSelect,
|
||||||
|
#excelMapSelect,
|
||||||
|
#legacyTemplateSelect {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 7px 34px 7px 10px;
|
||||||
|
border: 1px solid #9f9f9f;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #eeeef4;
|
||||||
|
color: #222;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.25;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#documentTypeSelect {
|
||||||
|
max-width: 390px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#excelMapSelect {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#legacyTemplateSelect {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* small light-gray profile caption */
|
||||||
|
#documentTypeDescription,
|
||||||
|
.document-type-description,
|
||||||
|
.profile-description,
|
||||||
|
[data-role="document-type-description"] {
|
||||||
|
font-size: 0.82rem !important;
|
||||||
|
color: #8a8a8a !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
margin: 6px 0 14px 0 !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tighter Excel row */
|
||||||
|
#excelTransferRow,
|
||||||
|
.excel-transfer-row {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: minmax(260px, 1fr) auto auto auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
|
margin: 12px 0 18px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#excelTransferRow label,
|
||||||
|
.excel-transfer-row label {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#excelTransferRow .file-button,
|
||||||
|
.excel-transfer-row .file-button,
|
||||||
|
#downloadExcelTemplateButton,
|
||||||
|
#exportExcelButton {
|
||||||
|
min-height: 38px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* template selector below Excel row */
|
||||||
|
#legacyTemplateRow,
|
||||||
|
.template-picker-row {
|
||||||
|
margin: 0 0 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#legacyTemplateRow label,
|
||||||
|
.template-picker-row label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
#excelTransferRow,
|
||||||
|
.excel-transfer-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloadExcelTemplateButton,
|
||||||
|
#exportExcelButton,
|
||||||
|
#excelTransferRow .file-button,
|
||||||
|
.excel-transfer-row .file-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-manager {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-manager-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-name {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
color: #444;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-delete-button {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
background: #f6f6f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* final legal UI overrides */
|
||||||
|
#documentTypeSelect,
|
||||||
|
#excelMapSelect,
|
||||||
|
#legacyTemplateSelect,
|
||||||
|
.same-dropdown-style {
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: 38px !important;
|
||||||
|
padding: 7px 34px 7px 10px !important;
|
||||||
|
border: 1px solid #9f9f9f !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
background-color: #eeeef4 !important;
|
||||||
|
color: #222 !important;
|
||||||
|
font: inherit !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#documentTypeSelect {
|
||||||
|
max-width: 390px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legal-profile-caption {
|
||||||
|
font-size: 0.82rem !important;
|
||||||
|
color: #8a8a8a !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
margin: 6px 0 14px 0 !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#excelTransferRow,
|
||||||
|
.excel-transfer-row {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: minmax(260px, 1fr) auto auto auto !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
align-items: end !important;
|
||||||
|
margin: 12px 0 18px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#legacyTemplateRow,
|
||||||
|
.template-picker-row {
|
||||||
|
margin: 0 0 22px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-manager {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
padding-top: 10px !important;
|
||||||
|
border-top: 1px solid #ddd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-manager-title {
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #555 !important;
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-row {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
align-items: center !important;
|
||||||
|
padding: 5px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-template-name {
|
||||||
|
font-size: 0.86rem !important;
|
||||||
|
color: #444 !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-delete-button {
|
||||||
|
border: 1px solid #bbb !important;
|
||||||
|
background: #f6f6f6 !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
#excelTransferRow,
|
||||||
|
.excel-transfer-row {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,551 @@
|
||||||
|
{
|
||||||
|
"id": "legal_profile",
|
||||||
|
"label": "Legal Profile Excel Maps",
|
||||||
|
"maps": [
|
||||||
|
{
|
||||||
|
"id": "legacy_datafile",
|
||||||
|
"sourceName": "fieldToCellMap",
|
||||||
|
"label": "Legacy Datafile Template",
|
||||||
|
"description": "Generated from public/constants.js object fieldToCellMap.",
|
||||||
|
"template": "excel/legal_profile/template.xlsx",
|
||||||
|
"legacyTemplateCandidates": [
|
||||||
|
{
|
||||||
|
"label": "amortization.xlsx",
|
||||||
|
"legacyPath": "/mnt/storage/sftp/mcelwain/repository/word-doc-generator/amortization.xlsx",
|
||||||
|
"filename": "amortization.xlsx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "template.xlsx",
|
||||||
|
"legacyPath": "/mnt/storage/sftp/mcelwain/repository/word-doc-generator/template.xlsx",
|
||||||
|
"filename": "template.xlsx"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {
|
||||||
|
"debtCollector6AddressLine1": "A100",
|
||||||
|
"debtCollector7Name": "A104",
|
||||||
|
"debtCollector7AddressLine1": "A106",
|
||||||
|
"SSNLastFour": "A11",
|
||||||
|
"debtCollector8Name": "A110",
|
||||||
|
"debtCollector8AddressLine1": "A112",
|
||||||
|
"debtCollector9Name": "A116",
|
||||||
|
"debtCollector9AddressLine1": "A118",
|
||||||
|
"debtCollector10Name": "A122",
|
||||||
|
"debtCollector10AddressLine1": "A124",
|
||||||
|
"debtCollector11Name": "A128",
|
||||||
|
"debtCollector11AddressLine1": "A130",
|
||||||
|
"debtCollector12Name": "A134",
|
||||||
|
"debtCollector12AddressLine1": "A136",
|
||||||
|
"client2FirstName": "A14",
|
||||||
|
"debtCollector13Name": "A140",
|
||||||
|
"debtCollector13AddressLine1": "A142",
|
||||||
|
"debtCollector14Name": "A146",
|
||||||
|
"debtCollector14AddressLine1": "A148",
|
||||||
|
"debtCollector15Name": "A152",
|
||||||
|
"debtCollector15AddressLine1": "A154",
|
||||||
|
"debtCollector16Name": "A158",
|
||||||
|
"client2homeAddress": "A16",
|
||||||
|
"debtCollector16AddressLine1": "A160",
|
||||||
|
"debtCollector17Name": "A164",
|
||||||
|
"debtCollector17AddressLine1": "A166",
|
||||||
|
"debtCollector18Name": "A170",
|
||||||
|
"debtCollector18AddressLine1": "A172",
|
||||||
|
"debtCollector19Name": "A176",
|
||||||
|
"debtCollector19AddressLine1": "A178",
|
||||||
|
"client2homeCounty": "A18",
|
||||||
|
"debtCollector20Name": "A182",
|
||||||
|
"debtCollector20AddressLine1": "A184",
|
||||||
|
"debtCollector21Name": "A188",
|
||||||
|
"debtCollector21AddressLine1": "A190",
|
||||||
|
"debtCollector22Name": "A194",
|
||||||
|
"debtCollector22AddressLine1": "A196",
|
||||||
|
"client2dob": "A20",
|
||||||
|
"debtCollector23Name": "A200",
|
||||||
|
"debtCollector23AddressLine1": "A202",
|
||||||
|
"debtCollector24Name": "A206",
|
||||||
|
"debtCollector24AddressLine1": "A208",
|
||||||
|
"debtCollector25Name": "A212",
|
||||||
|
"debtCollector25AddressLine1": "A214",
|
||||||
|
"debtCollector26Name": "A218",
|
||||||
|
"SSN2LastFour": "A22",
|
||||||
|
"debtCollector26AddressLine1": "A220",
|
||||||
|
"debtCollector27Name": "A224",
|
||||||
|
"debtCollector27AddressLine1": "A226",
|
||||||
|
"debtCollector28Name": "A230",
|
||||||
|
"debtCollector28AddressLine1": "A232",
|
||||||
|
"debtCollector29Name": "A236",
|
||||||
|
"debtCollector29AddressLine1": "A238",
|
||||||
|
"debtCollector30Name": "A242",
|
||||||
|
"debtCollector30AddressLine1": "A244",
|
||||||
|
"caseDesignation": "A27",
|
||||||
|
"caseNumber": "A29",
|
||||||
|
"clientFirstName": "A3",
|
||||||
|
"caseSuitAmount": "A31",
|
||||||
|
"caseAnswerDate": "A33",
|
||||||
|
"caseFilingDate": "A35",
|
||||||
|
"caseAnswerFiledDate": "A37",
|
||||||
|
"settlementAmount": "A46",
|
||||||
|
"homeAddress": "A5",
|
||||||
|
"fee": "A54",
|
||||||
|
"nameOnCard": "A56",
|
||||||
|
"billingAddress": "A58",
|
||||||
|
"notes": "A63",
|
||||||
|
"debtCollector1Name": "A68",
|
||||||
|
"homeCounty": "A7",
|
||||||
|
"debtCollector1AddressLine1": "A70",
|
||||||
|
"debtCollector2Name": "A74",
|
||||||
|
"debtCollector2AddressLine1": "A76",
|
||||||
|
"debtCollector3Name": "A80",
|
||||||
|
"debtCollector3AddressLine1": "A82",
|
||||||
|
"debtCollector4Name": "A86",
|
||||||
|
"debtCollector4AddressLine1": "A88",
|
||||||
|
"dob": "A9",
|
||||||
|
"debtCollector5Name": "A92",
|
||||||
|
"debtCollector5AddressLine1": "A94",
|
||||||
|
"debtCollector6Name": "A98",
|
||||||
|
"debtCollector7Creditor": "B104",
|
||||||
|
"dmcName": "B11",
|
||||||
|
"debtCollector8Creditor": "B110",
|
||||||
|
"debtCollector9Creditor": "B116",
|
||||||
|
"debtCollector10Creditor": "B122",
|
||||||
|
"debtCollector11Creditor": "B128",
|
||||||
|
"debtCollector12Creditor": "B134",
|
||||||
|
"client2MiddleName": "B14",
|
||||||
|
"debtCollector13Creditor": "B140",
|
||||||
|
"debtCollector14Creditor": "B146",
|
||||||
|
"debtCollector15Creditor": "B152",
|
||||||
|
"debtCollector16Creditor": "B158",
|
||||||
|
"client2homeCity": "B16",
|
||||||
|
"debtCollector17Creditor": "B164",
|
||||||
|
"debtCollector18Creditor": "B170",
|
||||||
|
"debtCollector19Creditor": "B176",
|
||||||
|
"client2homePhone": "B18",
|
||||||
|
"debtCollector20Creditor": "B182",
|
||||||
|
"debtCollector21Creditor": "B188",
|
||||||
|
"debtCollector22Creditor": "B194",
|
||||||
|
"client2alias": "B20",
|
||||||
|
"debtCollector23Creditor": "B200",
|
||||||
|
"debtCollector24Creditor": "B206",
|
||||||
|
"debtCollector25Creditor": "B212",
|
||||||
|
"debtCollector26Creditor": "B218",
|
||||||
|
"debtCollector27Creditor": "B224",
|
||||||
|
"debtCollector28Creditor": "B230",
|
||||||
|
"debtCollector29Creditor": "B236",
|
||||||
|
"debtCollector30Creditor": "B242",
|
||||||
|
"caseCounty": "B27",
|
||||||
|
"casePlaintiff": "B29",
|
||||||
|
"clientMiddleName": "B3",
|
||||||
|
"caseSuitTheory": "B31",
|
||||||
|
"caseDivisionNumber": "B33",
|
||||||
|
"caseFilingAttorney": "B35",
|
||||||
|
"caseDisposition": "B37",
|
||||||
|
"settlementInstallmentAmount": "B46",
|
||||||
|
"homeCity": "B5",
|
||||||
|
"installmentAmount": "B54",
|
||||||
|
"cardNumber": "B56",
|
||||||
|
"billingZip": "B58",
|
||||||
|
"debtCollector1Creditor": "B68",
|
||||||
|
"homePhone": "B7",
|
||||||
|
"debtCollector2Creditor": "B74",
|
||||||
|
"debtCollector3Creditor": "B80",
|
||||||
|
"debtCollector4Creditor": "B86",
|
||||||
|
"alias": "B9",
|
||||||
|
"debtCollector5Creditor": "B92",
|
||||||
|
"debtCollector6Creditor": "B98",
|
||||||
|
"debtCollector6AddressLine2": "C100",
|
||||||
|
"debtCollector7Account": "C104",
|
||||||
|
"debtCollector7AddressLine2": "C106",
|
||||||
|
"debtCollector8Account": "C110",
|
||||||
|
"debtCollector8AddressLine2": "C112",
|
||||||
|
"debtCollector9Account": "C116",
|
||||||
|
"debtCollector9AddressLine2": "C118",
|
||||||
|
"debtCollector10Account": "C122",
|
||||||
|
"debtCollector10AddressLine2": "C124",
|
||||||
|
"debtCollector11Account": "C128",
|
||||||
|
"debtCollector11AddressLine2": "C130",
|
||||||
|
"debtCollector12Account": "C134",
|
||||||
|
"debtCollector12AddressLine2": "C136",
|
||||||
|
"client2LastName": "C14",
|
||||||
|
"debtCollector13Account": "C140",
|
||||||
|
"debtCollector13AddressLine2": "C142",
|
||||||
|
"debtCollector14Account": "C146",
|
||||||
|
"debtCollector14AddressLine2": "C148",
|
||||||
|
"debtCollector15Account": "C152",
|
||||||
|
"debtCollector15AddressLine2": "C154",
|
||||||
|
"debtCollector16Account": "C158",
|
||||||
|
"client2homeState": "C16",
|
||||||
|
"debtCollector16AddressLine2": "C160",
|
||||||
|
"debtCollector17Account": "C164",
|
||||||
|
"debtCollector17AddressLine2": "C166",
|
||||||
|
"debtCollector18Account": "C170",
|
||||||
|
"debtCollector18AddressLine2": "C172",
|
||||||
|
"debtCollector19Account": "C176",
|
||||||
|
"debtCollector19AddressLine2": "C178",
|
||||||
|
"client2cellPhone": "C18",
|
||||||
|
"debtCollector20Account": "C182",
|
||||||
|
"debtCollector20AddressLine2": "C184",
|
||||||
|
"debtCollector21Account": "C188",
|
||||||
|
"debtCollector21AddressLine2": "C190",
|
||||||
|
"debtCollector22Account": "C194",
|
||||||
|
"debtCollector22AddressLine2": "C196",
|
||||||
|
"client2NamePrefix": "C20",
|
||||||
|
"debtCollector23Account": "C200",
|
||||||
|
"debtCollector23AddressLine2": "C202",
|
||||||
|
"debtCollector24Account": "C206",
|
||||||
|
"debtCollector24AddressLine2": "C208",
|
||||||
|
"debtCollector25Account": "C212",
|
||||||
|
"debtCollector25AddressLine2": "C214",
|
||||||
|
"debtCollector26Account": "C218",
|
||||||
|
"debtCollector26AddressLine2": "C220",
|
||||||
|
"debtCollector27Account": "C224",
|
||||||
|
"debtCollector27AddressLine2": "C226",
|
||||||
|
"debtCollector28Account": "C230",
|
||||||
|
"debtCollector28AddressLine2": "C232",
|
||||||
|
"debtCollector29Account": "C236",
|
||||||
|
"debtCollector29AddressLine2": "C238",
|
||||||
|
"debtCollector30Account": "C242",
|
||||||
|
"debtCollector30AddressLine2": "C244",
|
||||||
|
"caseState": "C27",
|
||||||
|
"caseDefendant": "C29",
|
||||||
|
"clientLastName": "C3",
|
||||||
|
"caseOriginalCreditor": "C31",
|
||||||
|
"caseDivisionJudge": "C33",
|
||||||
|
"caseAccLastFour": "C35",
|
||||||
|
"caseDispositionDate": "C37",
|
||||||
|
"settlementFirstPaymentDate": "C46",
|
||||||
|
"homeState": "C5",
|
||||||
|
"installmentDate": "C54",
|
||||||
|
"securityCode": "C56",
|
||||||
|
"debtCollector1Account": "C68",
|
||||||
|
"cellPhone": "C7",
|
||||||
|
"debtCollector1AddressLine2": "C70",
|
||||||
|
"debtCollector2Account": "C74",
|
||||||
|
"debtCollector2AddressLine2": "C76",
|
||||||
|
"debtCollector3Account": "C80",
|
||||||
|
"debtCollector3AddressLine2": "C82",
|
||||||
|
"debtCollector4Account": "C86",
|
||||||
|
"debtCollector4AddressLine2": "C88",
|
||||||
|
"clientNamePrefix": "C9",
|
||||||
|
"debtCollector5Account": "C92",
|
||||||
|
"debtCollector5AddressLine2": "C94",
|
||||||
|
"debtCollector6Account": "C98",
|
||||||
|
"debtCollector7Amount": "D104",
|
||||||
|
"debtCollector8Amount": "D110",
|
||||||
|
"debtCollector9Amount": "D116",
|
||||||
|
"debtCollector10Amount": "D122",
|
||||||
|
"debtCollector11Amount": "D128",
|
||||||
|
"debtCollector12Amount": "D134",
|
||||||
|
"SSN2": "D14",
|
||||||
|
"debtCollector13Amount": "D140",
|
||||||
|
"debtCollector14Amount": "D146",
|
||||||
|
"debtCollector15Amount": "D152",
|
||||||
|
"debtCollector16Amount": "D158",
|
||||||
|
"client2homeZip": "D16",
|
||||||
|
"debtCollector17Amount": "D164",
|
||||||
|
"debtCollector18Amount": "D170",
|
||||||
|
"debtCollector19Amount": "D176",
|
||||||
|
"client2email": "D18",
|
||||||
|
"debtCollector20Amount": "D182",
|
||||||
|
"debtCollector21Amount": "D188",
|
||||||
|
"debtCollector22Amount": "D194",
|
||||||
|
"client2NameSuffix": "D20",
|
||||||
|
"debtCollector23Amount": "D200",
|
||||||
|
"debtCollector24Amount": "D206",
|
||||||
|
"debtCollector25Amount": "D212",
|
||||||
|
"debtCollector26Amount": "D218",
|
||||||
|
"debtCollector27Amount": "D224",
|
||||||
|
"debtCollector28Amount": "D230",
|
||||||
|
"debtCollector29Amount": "D236",
|
||||||
|
"debtCollector30Amount": "D242",
|
||||||
|
"caseDivisionDesignation": "D27",
|
||||||
|
"caseOpposingCounsel": "D29",
|
||||||
|
"SSN": "D3",
|
||||||
|
"caseAccountNumber": "D31",
|
||||||
|
"discoCosDate": "D33",
|
||||||
|
"caseOCFileNumber": "D35",
|
||||||
|
"settlementInstallmentNo": "D46",
|
||||||
|
"homeZip": "D5",
|
||||||
|
"expiration": "D56",
|
||||||
|
"debtCollector1Amount": "D68",
|
||||||
|
"email": "D7",
|
||||||
|
"debtCollector2Amount": "D74",
|
||||||
|
"debtCollector3Amount": "D80",
|
||||||
|
"debtCollector4Amount": "D86",
|
||||||
|
"clientNameSuffix": "D9",
|
||||||
|
"debtCollector5Amount": "D92",
|
||||||
|
"debtCollector6Amount": "D98"
|
||||||
|
},
|
||||||
|
"mode": "datafile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "legacy_datafile_old",
|
||||||
|
"sourceName": "fieldToCellMapOld",
|
||||||
|
"label": "Legacy Datafile Template - Old Map",
|
||||||
|
"description": "Generated from public/constants.js object fieldToCellMapOld.",
|
||||||
|
"template": "excel/legal_profile/template.xlsx",
|
||||||
|
"legacyTemplateCandidates": [
|
||||||
|
{
|
||||||
|
"label": "amortization.xlsx",
|
||||||
|
"legacyPath": "/mnt/storage/sftp/mcelwain/repository/word-doc-generator/amortization.xlsx",
|
||||||
|
"filename": "amortization.xlsx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "template.xlsx",
|
||||||
|
"legacyPath": "/mnt/storage/sftp/mcelwain/repository/word-doc-generator/template.xlsx",
|
||||||
|
"filename": "template.xlsx"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {
|
||||||
|
"debtCollector12Name": "A101",
|
||||||
|
"debtCollector12AddressLine1": "A103",
|
||||||
|
"debtCollector13Name": "A105",
|
||||||
|
"debtCollector13AddressLine1": "A107",
|
||||||
|
"debtCollector14Name": "A109",
|
||||||
|
"dob": "A11",
|
||||||
|
"debtCollector14AddressLine1": "A111",
|
||||||
|
"debtCollector15Name": "A113",
|
||||||
|
"debtCollector15AddressLine1": "A115",
|
||||||
|
"debtCollector16Name": "A117",
|
||||||
|
"debtCollector16AddressLine1": "A119",
|
||||||
|
"debtCollector17Name": "A121",
|
||||||
|
"debtCollector17AddressLine1": "A123",
|
||||||
|
"debtCollector18Name": "A125",
|
||||||
|
"debtCollector18AddressLine1": "A127",
|
||||||
|
"debtCollector19Name": "A129",
|
||||||
|
"debtCollector19AddressLine1": "A131",
|
||||||
|
"debtCollector20Name": "A133",
|
||||||
|
"debtCollector20AddressLine1": "A135",
|
||||||
|
"debtCollector21Name": "A137",
|
||||||
|
"debtCollector21AddressLine1": "A139",
|
||||||
|
"debtCollector22Name": "A141",
|
||||||
|
"debtCollector22AddressLine1": "A143",
|
||||||
|
"debtCollector23Name": "A145",
|
||||||
|
"debtCollector23AddressLine1": "A147",
|
||||||
|
"debtCollector24Name": "A149",
|
||||||
|
"caseDesignation": "A15",
|
||||||
|
"debtCollector24AddressLine1": "A151",
|
||||||
|
"debtCollector25Name": "A153",
|
||||||
|
"debtCollector25AddressLine1": "A155",
|
||||||
|
"debtCollector26Name": "A157",
|
||||||
|
"debtCollector26AddressLine1": "A159",
|
||||||
|
"debtCollector27Name": "A161",
|
||||||
|
"debtCollector27AddressLine1": "A163",
|
||||||
|
"debtCollector28Name": "A165",
|
||||||
|
"debtCollector28AddressLine1": "A167",
|
||||||
|
"debtCollector29Name": "A169",
|
||||||
|
"caseNumber": "A17",
|
||||||
|
"debtCollector29AddressLine1": "A171",
|
||||||
|
"caseSuitAmount": "A19",
|
||||||
|
"caseAnswerDate": "A21",
|
||||||
|
"fee": "A25",
|
||||||
|
"nameOnCard": "A27",
|
||||||
|
"billingAddress": "A29",
|
||||||
|
"clientFirstName": "A3",
|
||||||
|
"settlementAmount": "A32",
|
||||||
|
"debtCollector1Name": "A39",
|
||||||
|
"debtCollector1AddressLine1": "A41",
|
||||||
|
"debtCollector2Name": "A45",
|
||||||
|
"debtCollector2AddressLine1": "A47",
|
||||||
|
"client2FirstName": "A5",
|
||||||
|
"debtCollector3Name": "A51",
|
||||||
|
"debtCollector3AddressLine1": "A53",
|
||||||
|
"debtCollector4Name": "A57",
|
||||||
|
"debtCollector4AddressLine1": "A59",
|
||||||
|
"debtCollector5Name": "A63",
|
||||||
|
"debtCollector5AddressLine1": "A65",
|
||||||
|
"debtCollector6Name": "A69",
|
||||||
|
"homeAddress": "A7",
|
||||||
|
"debtCollector6AddressLine1": "A71",
|
||||||
|
"debtCollector7Name": "A75",
|
||||||
|
"debtCollector7AddressLine1": "A77",
|
||||||
|
"debtCollector8Name": "A81",
|
||||||
|
"debtCollector8AddressLine1": "A83",
|
||||||
|
"debtCollector9Name": "A87",
|
||||||
|
"debtCollector9AddressLine1": "A89",
|
||||||
|
"homeCounty": "A9",
|
||||||
|
"debtCollector10Name": "A91",
|
||||||
|
"debtCollector10AddressLine1": "A93",
|
||||||
|
"debtCollector11Name": "A95",
|
||||||
|
"debtCollector11AddressLine1": "A97",
|
||||||
|
"debtCollector12Creditor": "B101",
|
||||||
|
"debtCollector13Creditor": "B105",
|
||||||
|
"debtCollector14Creditor": "B109",
|
||||||
|
"alias": "B11",
|
||||||
|
"debtCollector15Creditor": "B113",
|
||||||
|
"debtCollector16Creditor": "B117",
|
||||||
|
"debtCollector17Creditor": "B121",
|
||||||
|
"debtCollector18Creditor": "B125",
|
||||||
|
"debtCollector19Creditor": "B129",
|
||||||
|
"debtCollector20Creditor": "B133",
|
||||||
|
"debtCollector21Creditor": "B137",
|
||||||
|
"debtCollector22Creditor": "B141",
|
||||||
|
"debtCollector23Creditor": "B145",
|
||||||
|
"debtCollector24Creditor": "B149",
|
||||||
|
"caseCounty": "B15",
|
||||||
|
"debtCollector25Creditor": "B153",
|
||||||
|
"debtCollector26Creditor": "B157",
|
||||||
|
"debtCollector27Creditor": "B161",
|
||||||
|
"debtCollector28Creditor": "B165",
|
||||||
|
"debtCollector29Creditor": "B169",
|
||||||
|
"casePlaintiff": "B17",
|
||||||
|
"caseSuitTheory": "B19",
|
||||||
|
"caseDivisionNumber": "B21",
|
||||||
|
"installmentAmount": "B25",
|
||||||
|
"cardNumber": "B27",
|
||||||
|
"billingZip": "B29",
|
||||||
|
"clientMiddleName": "B3",
|
||||||
|
"notes": "B31",
|
||||||
|
"settlementInstallmentAmount": "B32",
|
||||||
|
"debtCollector1Creditor": "B39",
|
||||||
|
"debtCollector2Creditor": "B45",
|
||||||
|
"client2MiddleName": "B5",
|
||||||
|
"debtCollector3Creditor": "B51",
|
||||||
|
"debtCollector4Creditor": "B57",
|
||||||
|
"debtCollector5Creditor": "B63",
|
||||||
|
"debtCollector6Creditor": "B69",
|
||||||
|
"homeCity": "B7",
|
||||||
|
"debtCollector7Creditor": "B75",
|
||||||
|
"debtCollector8Creditor": "B81",
|
||||||
|
"debtCollector9Creditor": "B87",
|
||||||
|
"homePhone": "B9",
|
||||||
|
"debtCollector10Creditor": "B91",
|
||||||
|
"debtCollector11Creditor": "B95",
|
||||||
|
"debtCollector12Account": "C101",
|
||||||
|
"debtCollector12AddressLine2": "C103",
|
||||||
|
"debtCollector13Account": "C105",
|
||||||
|
"debtCollector13AddressLine2": "C107",
|
||||||
|
"debtCollector14Account": "C109",
|
||||||
|
"clientNameSuffix": "C11",
|
||||||
|
"debtCollector14AddressLine2": "C111",
|
||||||
|
"debtCollector15Account": "C113",
|
||||||
|
"debtCollector15AddressLine2": "C115",
|
||||||
|
"debtCollector16Account": "C117",
|
||||||
|
"debtCollector16AddressLine2": "C119",
|
||||||
|
"debtCollector17Account": "C121",
|
||||||
|
"debtCollector17AddressLine2": "C123",
|
||||||
|
"debtCollector18Account": "C125",
|
||||||
|
"debtCollector18AddressLine2": "C127",
|
||||||
|
"debtCollector19Account": "C129",
|
||||||
|
"debtCollector19AddressLine2": "C131",
|
||||||
|
"debtCollector20Account": "C133",
|
||||||
|
"debtCollector20AddressLine2": "C135",
|
||||||
|
"debtCollector21Account": "C137",
|
||||||
|
"debtCollector21AddressLine2": "C139",
|
||||||
|
"debtCollector22Account": "C141",
|
||||||
|
"debtCollector22AddressLine2": "C143",
|
||||||
|
"debtCollector23Account": "C145",
|
||||||
|
"debtCollector23AddressLine2": "C147",
|
||||||
|
"debtCollector24Account": "C149",
|
||||||
|
"caseState": "C15",
|
||||||
|
"debtCollector24AddressLine2": "C151",
|
||||||
|
"debtCollector25Account": "C153",
|
||||||
|
"debtCollector25AddressLine2": "C155",
|
||||||
|
"debtCollector26Account": "C157",
|
||||||
|
"debtCollector26AddressLine2": "C159",
|
||||||
|
"debtCollector27Account": "C161",
|
||||||
|
"debtCollector27AddressLine2": "C163",
|
||||||
|
"debtCollector28Account": "C165",
|
||||||
|
"debtCollector28AddressLine2": "C167",
|
||||||
|
"debtCollector29Account": "C169",
|
||||||
|
"caseDefendant": "C17",
|
||||||
|
"debtCollector29AddressLine2": "C171",
|
||||||
|
"caseOriginalCreditor": "C19",
|
||||||
|
"caseDivisionJudge": "C21",
|
||||||
|
"installmentDate": "C25",
|
||||||
|
"securityCode": "C27",
|
||||||
|
"clientLastName": "C3",
|
||||||
|
"settlementFirstPaymentDate": "C32",
|
||||||
|
"debtCollector1Account": "C39",
|
||||||
|
"debtCollector1AddressLine2": "C41",
|
||||||
|
"debtCollector2Account": "C45",
|
||||||
|
"debtCollector2AddressLine2": "C47",
|
||||||
|
"client2LastName": "C5",
|
||||||
|
"debtCollector3Account": "C51",
|
||||||
|
"debtCollector3AddressLine2": "C53",
|
||||||
|
"debtCollector4Account": "C57",
|
||||||
|
"debtCollector4AddressLine2": "C59",
|
||||||
|
"debtCollector5Account": "C63",
|
||||||
|
"debtCollector5AddressLine2": "C65",
|
||||||
|
"debtCollector6Account": "C69",
|
||||||
|
"homeState": "C7",
|
||||||
|
"debtCollector6AddressLine2": "C71",
|
||||||
|
"debtCollector7Account": "C75",
|
||||||
|
"debtCollector7AddressLine2": "C77",
|
||||||
|
"debtCollector8Account": "C81",
|
||||||
|
"debtCollector8AddressLine2": "C83",
|
||||||
|
"debtCollector9Account": "C87",
|
||||||
|
"debtCollector9AddressLine2": "C89",
|
||||||
|
"cellPhone": "C9",
|
||||||
|
"debtCollector10Account": "C91",
|
||||||
|
"debtCollector10AddressLine2": "C93",
|
||||||
|
"debtCollector11Account": "C95",
|
||||||
|
"debtCollector11AddressLine2": "C97",
|
||||||
|
"debtCollector12Amount": "D101",
|
||||||
|
"debtCollector13Amount": "D105",
|
||||||
|
"debtCollector14Amount": "D109",
|
||||||
|
"debtCollector15Amount": "D113",
|
||||||
|
"debtCollector16Amount": "D117",
|
||||||
|
"debtCollector17Amount": "D121",
|
||||||
|
"debtCollector18Amount": "D125",
|
||||||
|
"debtCollector19Amount": "D129",
|
||||||
|
"debtCollector20Amount": "D133",
|
||||||
|
"debtCollector21Amount": "D137",
|
||||||
|
"debtCollector22Amount": "D141",
|
||||||
|
"debtCollector23Amount": "D145",
|
||||||
|
"debtCollector24Amount": "D149",
|
||||||
|
"caseDivisionDesignation": "D15",
|
||||||
|
"debtCollector25Amount": "D153",
|
||||||
|
"debtCollector26Amount": "D157",
|
||||||
|
"debtCollector27Amount": "D161",
|
||||||
|
"debtCollector28Amount": "D165",
|
||||||
|
"debtCollector29Amount": "D169",
|
||||||
|
"caseOpposingCounsel": "D17",
|
||||||
|
"caseAccountNumber": "D19",
|
||||||
|
"discoCosDate": "D21",
|
||||||
|
"expiration": "D27",
|
||||||
|
"SSN": "D3",
|
||||||
|
"settlementInstallmentNo": "D32",
|
||||||
|
"debtCollector1Amount": "D39",
|
||||||
|
"debtCollector2Amount": "D45",
|
||||||
|
"SSN2": "D5",
|
||||||
|
"debtCollector3Amount": "D51",
|
||||||
|
"debtCollector4Amount": "D57",
|
||||||
|
"debtCollector5Amount": "D63",
|
||||||
|
"debtCollector6Amount": "D69",
|
||||||
|
"homeZip": "D7",
|
||||||
|
"debtCollector7Amount": "D75",
|
||||||
|
"debtCollector8Amount": "D81",
|
||||||
|
"debtCollector9Amount": "D87",
|
||||||
|
"email": "D9",
|
||||||
|
"debtCollector10Amount": "D91",
|
||||||
|
"debtCollector11Amount": "D95"
|
||||||
|
},
|
||||||
|
"mode": "datafile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "settlement_amortization",
|
||||||
|
"label": "Settlement Amortization Calculator",
|
||||||
|
"description": "Writes settlement values into amortization.xlsx for a quick amortization check.",
|
||||||
|
"template": "excel/legal_profile/amortization.xlsx",
|
||||||
|
"mode": "calculator",
|
||||||
|
"fields": [
|
||||||
|
"casePlaintiff",
|
||||||
|
"settlementAmount",
|
||||||
|
"settlementFirstPaymentDate",
|
||||||
|
"settlementInstallmentAmount",
|
||||||
|
"settlementInstallmentNo",
|
||||||
|
"settlementInterestRate",
|
||||||
|
"settlementPaymentsPerYear"
|
||||||
|
],
|
||||||
|
"field_to_cell": {
|
||||||
|
"settlementAmount": "E3",
|
||||||
|
"settlementInterestRate": "E4",
|
||||||
|
"settlementInstallmentNo": "E5",
|
||||||
|
"settlementPaymentsPerYear": "E6",
|
||||||
|
"settlementFirstPaymentDate": "E7",
|
||||||
|
"settlementInstallmentAmount": "I3",
|
||||||
|
"casePlaintiff": "H9"
|
||||||
|
},
|
||||||
|
"field_count": 7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,211 @@
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
from tools.doc_generator.logic.calculations import apply_calculations
|
||||||
|
from tools.doc_generator.logic.core_fields import merge_core_fields
|
||||||
|
from tools.doc_generator.logic.document_types import get_document_type
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
PROJECT_ROOT = BASE_DIR.parent.parent
|
||||||
|
CONTENT_DIR = BASE_DIR / "content"
|
||||||
|
|
||||||
|
EXCEL_MAPS_DIR = CONTENT_DIR / "excel_maps"
|
||||||
|
TEMPLATES_DIR = CONTENT_DIR / "templates"
|
||||||
|
EXCEL_TEMPLATE_DIR = TEMPLATES_DIR / "legacy_excel"
|
||||||
|
OLD_WORD_DOC_GENERATOR_DIR = Path("/mnt/storage/sftp/mcelwain/repository/word-doc-generator")
|
||||||
|
EXPORTS_DIR = PROJECT_ROOT / "exports"
|
||||||
|
|
||||||
|
MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
ET.register_namespace("", MAIN_NS)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_filename(value: str) -> str:
|
||||||
|
value = str(value or "legacy_export").strip()
|
||||||
|
value = re.sub(r"[^A-Za-z0-9._ -]+", "", value)
|
||||||
|
value = re.sub(r"\s+", "_", value)
|
||||||
|
return value or "legacy_export"
|
||||||
|
|
||||||
|
|
||||||
|
def get_excel_map_set_id(document_type_id: str) -> str:
|
||||||
|
document_type = get_document_type(document_type_id)
|
||||||
|
return document_type.get("excelMapSet") or document_type_id
|
||||||
|
|
||||||
|
|
||||||
|
def load_excel_map_set(document_type_id: str) -> dict:
|
||||||
|
map_set_id = get_excel_map_set_id(document_type_id)
|
||||||
|
path = EXCEL_MAPS_DIR / f"{map_set_id}.json"
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Excel map set not found: {path}")
|
||||||
|
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_excel_maps(document_type_id: str) -> list[dict]:
|
||||||
|
return load_excel_map_set(document_type_id).get("maps", [])
|
||||||
|
|
||||||
|
|
||||||
|
def load_excel_map(document_type_id: str, map_id: str) -> dict:
|
||||||
|
for item in load_excel_maps(document_type_id):
|
||||||
|
if item.get("id") == map_id:
|
||||||
|
return item
|
||||||
|
|
||||||
|
raise FileNotFoundError(f"Excel map not found for {document_type_id}: {map_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_excel_template(template_name: str) -> Path:
|
||||||
|
template_name = str(template_name or "").strip()
|
||||||
|
template_basename = Path(template_name).name
|
||||||
|
|
||||||
|
# Prefer explicit profile-library template paths from the map file.
|
||||||
|
candidate_paths = [
|
||||||
|
TEMPLATES_DIR / template_name,
|
||||||
|
CONTENT_DIR / "templates" / template_name,
|
||||||
|
|
||||||
|
# Backward-compatible fallbacks.
|
||||||
|
EXCEL_TEMPLATE_DIR / template_basename,
|
||||||
|
OLD_WORD_DOC_GENERATOR_DIR / template_basename,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Last-resort recursive lookups.
|
||||||
|
candidate_paths.extend(sorted(TEMPLATES_DIR.rglob(template_basename)))
|
||||||
|
candidate_paths.extend(sorted(OLD_WORD_DOC_GENERATOR_DIR.rglob(template_basename)))
|
||||||
|
|
||||||
|
template_path = next((candidate for candidate in candidate_paths if candidate.exists()), None)
|
||||||
|
|
||||||
|
if template_path is None:
|
||||||
|
searched = "\n".join(str(candidate) for candidate in candidate_paths[:30])
|
||||||
|
raise FileNotFoundError(f"Excel template not found: {template_name}. Searched:\n{searched}")
|
||||||
|
|
||||||
|
return template_path
|
||||||
|
|
||||||
|
|
||||||
|
def cell_parts(cell_ref: str):
|
||||||
|
col = "".join(ch for ch in cell_ref if ch.isalpha()).upper()
|
||||||
|
row = int("".join(ch for ch in cell_ref if ch.isdigit()))
|
||||||
|
return col, row
|
||||||
|
|
||||||
|
|
||||||
|
def col_number(col: str) -> int:
|
||||||
|
total = 0
|
||||||
|
for ch in col:
|
||||||
|
total = total * 26 + (ord(ch) - ord("A") + 1)
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def cell_sort_key(cell_ref: str):
|
||||||
|
col, row = cell_parts(cell_ref)
|
||||||
|
return row, col_number(col)
|
||||||
|
|
||||||
|
|
||||||
|
def find_or_create_row(sheet_data, row_number: int):
|
||||||
|
ns = f"{{{MAIN_NS}}}"
|
||||||
|
rows = sheet_data.findall(ns + "row")
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
if int(row.attrib.get("r", "0")) == row_number:
|
||||||
|
return row
|
||||||
|
|
||||||
|
new_row = ET.Element(ns + "row", {"r": str(row_number)})
|
||||||
|
|
||||||
|
inserted = False
|
||||||
|
for index, row in enumerate(rows):
|
||||||
|
if int(row.attrib.get("r", "0")) > row_number:
|
||||||
|
sheet_data.insert(index, new_row)
|
||||||
|
inserted = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not inserted:
|
||||||
|
sheet_data.append(new_row)
|
||||||
|
|
||||||
|
return new_row
|
||||||
|
|
||||||
|
|
||||||
|
def find_or_create_cell(row, cell_ref: str):
|
||||||
|
ns = f"{{{MAIN_NS}}}"
|
||||||
|
cells = row.findall(ns + "c")
|
||||||
|
|
||||||
|
for cell in cells:
|
||||||
|
if cell.attrib.get("r") == cell_ref:
|
||||||
|
return cell
|
||||||
|
|
||||||
|
new_cell = ET.Element(ns + "c", {"r": cell_ref})
|
||||||
|
|
||||||
|
inserted = False
|
||||||
|
for index, cell in enumerate(cells):
|
||||||
|
if cell_sort_key(cell.attrib.get("r", "A1")) > cell_sort_key(cell_ref):
|
||||||
|
row.insert(index, new_cell)
|
||||||
|
inserted = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not inserted:
|
||||||
|
row.append(new_cell)
|
||||||
|
|
||||||
|
return new_cell
|
||||||
|
|
||||||
|
|
||||||
|
def set_cell_text(cell, value):
|
||||||
|
ns = f"{{{MAIN_NS}}}"
|
||||||
|
|
||||||
|
for child in list(cell):
|
||||||
|
cell.remove(child)
|
||||||
|
|
||||||
|
cell.attrib.pop("s", None)
|
||||||
|
cell.attrib["t"] = "inlineStr"
|
||||||
|
|
||||||
|
inline = ET.SubElement(cell, ns + "is")
|
||||||
|
text = ET.SubElement(inline, ns + "t")
|
||||||
|
text.text = "" if value is None else str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def write_sheet1_values(xlsx_path: Path, output_path: Path, field_to_cell: dict, data: dict):
|
||||||
|
workbook = load_workbook(xlsx_path)
|
||||||
|
worksheet = workbook.worksheets[0]
|
||||||
|
|
||||||
|
for field, cell_ref in field_to_cell.items():
|
||||||
|
worksheet[cell_ref] = "" if data.get(field) is None else data.get(field, "")
|
||||||
|
|
||||||
|
# Ask Excel-compatible apps to recalculate formulas when the workbook opens.
|
||||||
|
try:
|
||||||
|
workbook.calculation.fullCalcOnLoad = True
|
||||||
|
workbook.calculation.forceFullCalc = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
workbook.save(output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def export_profile_excel(document_type_id: str, data: dict, map_id: str) -> Path:
|
||||||
|
document_type = get_document_type(document_type_id)
|
||||||
|
|
||||||
|
final_data = merge_core_fields(data)
|
||||||
|
final_data = apply_calculations(document_type, final_data)
|
||||||
|
|
||||||
|
excel_map = load_excel_map(document_type_id, map_id)
|
||||||
|
template_name = excel_map.get("template") or "template.xlsx"
|
||||||
|
template_path = resolve_excel_template(template_name)
|
||||||
|
|
||||||
|
EXPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
case_number = safe_filename(final_data.get("caseNumber") or "no_case_number")
|
||||||
|
timestamp = safe_filename(final_data.get("timestamp_YYYY-MM-DD_HH-mm-ss") or "export")
|
||||||
|
output_path = EXPORTS_DIR / f"{document_type_id}_{map_id}_{case_number}_{timestamp}.xlsx"
|
||||||
|
|
||||||
|
write_sheet1_values(
|
||||||
|
xlsx_path=template_path,
|
||||||
|
output_path=output_path,
|
||||||
|
field_to_cell=excel_map.get("fields", {}),
|
||||||
|
data=final_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
# Backward-compatible function name while routes are transitioning.
|
||||||
|
def export_legacy_excel(document_type_id: str, data: dict, map_id: str = "legacy_datafile") -> Path:
|
||||||
|
return export_profile_excel(document_type_id, data, map_id)
|
||||||
Loading…
Reference in New Issue