Compare commits

...

3 Commits

210 changed files with 4694 additions and 1060 deletions

View File

@ -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.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()
@ -18,11 +19,19 @@ INPUTS_DIR = PROJECT_ROOT / "inputs"
UPLOADS_DIR = INPUTS_DIR / "uploads"
JSON_UPLOADS_DIR = UPLOADS_DIR / "json"
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):
document_type_id: str
data: dict
template_id: str | None = None
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")
def generate_document(request: GenerateDocRequest):
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:
raise HTTPException(status_code=404, detail=str(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(
path=file_path,
filename=file_path.name,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
path,
filename=filename,
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"),
)

View File

@ -3,3 +3,4 @@ uvicorn[standard]
python-multipart
python-docx
pendulum
openpyxl

View File

@ -0,0 +1,137 @@
import json
import re
from pathlib import Path
OLD_APP = Path("/mnt/storage/sftp/mcelwain/repository/word-doc-generator")
OLD_CONSTANTS_CANDIDATES = [
OLD_APP / "public" / "constants.js",
OLD_APP / "constants.js",
]
OUT_DIR = Path("tools/doc_generator/content/excel_maps")
OUT_FILE = OUT_DIR / "legacy_excel_maps.json"
CELL_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\s*:\s*['\"]([A-Z]{1,3}[0-9]{1,5})['\"]")
def find_constants_file():
for path in OLD_CONSTANTS_CANDIDATES:
if path.exists():
return path
raise SystemExit("Could not find old constants.js")
def extract_object_blocks(text):
"""
Finds JS object-ish assignment/export blocks that contain Excel cell mappings.
This is intentionally simple and robust for the old constants.js style.
"""
blocks = []
# Match things like:
# const fieldToCellMap = { ... };
# export const fieldToCellMap = { ... };
# let someMap = { ... };
pattern = re.compile(
r"(?:export\s+)?(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\{",
re.MULTILINE,
)
for match in pattern.finditer(text):
name = match.group(1)
start = match.end() - 1
depth = 0
end = None
for i in range(start, len(text)):
char = text[i]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
end = i + 1
break
if end:
block = text[start:end]
cells = dict(CELL_RE.findall(block))
if cells:
blocks.append((name, cells))
return blocks
def label_from_name(name):
label = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
label = label.replace("_", " ").replace("-", " ")
label = re.sub(r"\s+", " ", label).strip()
return label.title()
def normalize_id(name):
value = re.sub(r"([a-z])([A-Z])", r"\1_\2", name)
value = re.sub(r"[^A-Za-z0-9]+", "_", value).strip("_").lower()
return value or "excel_map"
def discover_excel_templates():
templates = []
for path in sorted(OLD_APP.rglob("*.xlsx")):
if ".git" in path.parts or "node_modules" in path.parts:
continue
rel = path.relative_to(OLD_APP).as_posix()
templates.append({
"label": rel,
"legacyPath": str(path),
"filename": path.name
})
return templates
def main():
constants_path = find_constants_file()
text = constants_path.read_text(encoding="utf-8", errors="ignore")
blocks = extract_object_blocks(text)
excel_templates = discover_excel_templates()
maps = []
for name, cells in blocks:
map_id = normalize_id(name)
maps.append({
"id": map_id,
"sourceName": name,
"label": label_from_name(name),
"description": f"Generated from {constants_path.relative_to(OLD_APP)} object {name}.",
"template": excel_templates[0]["filename"] if excel_templates else "",
"legacyTemplateCandidates": excel_templates,
"fields": dict(sorted(cells.items(), key=lambda item: item[1]))
})
OUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_FILE.write_text(json.dumps({
"id": "legacy_excel_maps",
"source": str(constants_path),
"maps": maps
}, indent=2), encoding="utf-8")
print(f"Wrote {OUT_FILE}")
print(f"Source: {constants_path}")
print(f"Excel templates found: {len(excel_templates)}")
for t in excel_templates:
print(f"- {t['legacyPath']}")
print(f"Maps found: {len(maps)}")
for item in maps:
print(f"- {item['id']}: {len(item['fields'])} fields")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,192 @@
import json
import re
import subprocess
from pathlib import Path
OLD_APP = Path("/mnt/storage/sftp/mcelwain/repository/word-doc-generator")
DRAFT_PROFILE = Path("diagnostics/legacy_word_doc_generator_profile_draft.json")
OUT_PROFILE = Path("tools/doc_generator/content/document_types/legal_profile.json")
TEMPLATES_OUT = Path("tools/doc_generator/content/templates/legacy")
OLD_PUBLIC = OLD_APP / "public"
def run_node_list_extractor():
js = f"""
import {{ pathToFileURL }} from 'url';
const files = [
'casePlaintiffInfo.js',
'opposingCounselInfo.js',
'judgeInfo.js',
'caseFilingAttorneyInfo.js',
'filingAttorneyInfo.js',
'debtCollectorInfo.js'
];
const base = {json.dumps(str(OLD_PUBLIC))};
const result = {{}};
for (const file of files) {{
try {{
const mod = await import(pathToFileURL(`${{base}}/${{file}}`).href);
for (const [exportName, value] of Object.entries(mod)) {{
if (value && typeof value === 'object' && !Array.isArray(value)) {{
result[exportName] = Object.keys(value).sort();
}}
}}
}} catch (err) {{
// Some optional info files may not exist.
}}
}}
console.log(JSON.stringify(result));
"""
try:
completed = subprocess.run(
["node", "--input-type=module", "-e", js],
check=True,
capture_output=True,
text=True,
)
return json.loads(completed.stdout)
except Exception:
return {}
def nice_label(name):
label = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
label = label.replace("_", " ").replace("-", " ")
label = re.sub(r"\bSsn\b", "SSN", label.title())
label = label.replace("Dob", "DOB")
return label
def apply_legal_field_metadata(field):
name = field["name"]
lower = name.lower()
# Normalize generated labels.
field["label"] = nice_label(name)
# Autocomplete fields.
if name == "casePlaintiff":
field["type"] = "autocomplete"
field["list"] = "plaintiffs"
elif name == "caseOpposingCounsel":
field["type"] = "autocomplete"
field["list"] = "opposingCounsel"
elif name == "caseDivisionJudge":
field["type"] = "autocomplete"
field["list"] = "judges"
elif name == "caseFilingAttorney":
field["type"] = "autocomplete"
field["list"] = "filingAttorneys"
elif re.fullmatch(r"debtCollector\d+Name", name):
field["type"] = "autocomplete"
field["list"] = "debtCollectors"
elif lower.endswith("state") or name in {"caseState", "homeState", "client2homeState"}:
field["type"] = "autocomplete"
field["list"] = "states"
elif name == "caseDesignation":
field["type"] = "autocomplete"
field["list"] = "caseDesignations"
# Long text fields.
if name in {"notes", "caseAppearanceInfo", "paymentOptions", "paymentOptions1", "paymentOptions2", "paymentOptions3", "paymentOptions4", "paymentOptions5"}:
field["type"] = "textarea"
return field
def walk_sections(sections):
for section in sections:
section["collapsible"] = section.get("heading") not in {
"Client Information",
"Case Information",
}
section["defaultOpen"] = section.get("heading") in {
"Client Information",
"Case Information",
}
section["fields"] = [
apply_legal_field_metadata(field)
for field in section.get("fields", [])
]
for subsection in section.get("subsections", []):
walk_sections([subsection])
def discover_templates():
templates = []
for path in sorted(TEMPLATES_OUT.rglob("*.docx")):
rel = path.relative_to(Path("tools/doc_generator/content/templates")).as_posix()
template_id = re.sub(r"[^a-zA-Z0-9]+", "_", path.stem).strip("_").lower()
label = path.relative_to(TEMPLATES_OUT).as_posix()
label = label.replace(".docx", "")
label = label.replace("/", " / ")
label = label.replace("_", " ")
templates.append({
"id": template_id,
"label": label,
"template": rel,
"outputFilename": f"{template_id}_{{caseNumber}}_{{timestamp_YYYY-MM-DD_HH-mm-ss}}.docx"
})
return templates
def main():
if not DRAFT_PROFILE.exists():
raise SystemExit(f"Missing {DRAFT_PROFILE}. Run review-old-word-doc-generator.py first.")
draft = json.loads(DRAFT_PROFILE.read_text(encoding="utf-8"))
lists_raw = run_node_list_extractor()
lists = {
"plaintiffs": lists_raw.get("casePlaintiffInfo", []),
"opposingCounsel": lists_raw.get("caseOpposingCounselInfo", []) or lists_raw.get("opposingCounselInfo", []),
"judges": lists_raw.get("judgeInfo", []),
"filingAttorneys": lists_raw.get("caseFilingAttorneyInfo", []) or lists_raw.get("filingAttorneyInfo", []),
"debtCollectors": lists_raw.get("debtCollectorInfo", []),
"states": ["MO", "KS"],
"caseDesignations": [
"Associate Circuit",
"Circuit",
"Limited Actions",
"Small Claims"
]
}
sections = draft["sections"]
walk_sections(sections)
templates = discover_templates()
profile = {
"id": "legal_profile",
"name": "Legal Profile",
"description": "Consumer debt defense legal profile generated from the legacy word-doc-generator app.",
"template": templates[0]["template"] if templates else "legacy/Canned-Emails.docx",
"outputFilename": "legal_{caseNumber}_{timestamp_YYYY-MM-DD_HH-mm-ss}.docx",
"lists": lists,
"templates": templates,
"sections": sections
}
OUT_PROFILE.parent.mkdir(parents=True, exist_ok=True)
OUT_PROFILE.write_text(json.dumps(profile, indent=2), encoding="utf-8")
print(f"Wrote {OUT_PROFILE}")
print(f"Lists: {', '.join(f'{k}={len(v)}' for k, v in lists.items())}")
print(f"Templates: {len(templates)}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,337 @@
import json
import re
import subprocess
from pathlib import Path
OLD_APP = Path("/mnt/storage/sftp/mcelwain/repository/word-doc-generator")
OLD_PUBLIC = OLD_APP / "public"
OLD_HTML = OLD_PUBLIC / "index.html"
OUT_PROFILE = Path("tools/doc_generator/content/document_types/legal_profile.json")
TEMPLATES_OUT = Path("tools/doc_generator/content/templates/legacy")
TAG_RE = re.compile(r'<(input|select|textarea)\b[^>]*>', re.IGNORECASE | re.DOTALL)
ATTR_RE = re.compile(r'([a-zA-Z_:][-a-zA-Z0-9_:.]*)=["\']([^"\']*)["\']')
LABEL_RE = re.compile(
r'<label[^>]*for=["\']([^"\']+)["\'][^>]*>(.*?)</label>',
re.IGNORECASE | re.DOTALL
)
EXCLUDE_FIELD_NAMES = {
"letterTemplateFile",
"discoTemplateFile",
"excelFile",
"csvFile",
"templateFile",
"file",
"SSNLastFour",
"SSN2LastFour",
"caseAccLastFour",
"casePlaintiffFileName",
"caseAnswerDateString",
"caseAnswerDateYYYY-MM-DD",
"caseAnswerDateYyyyMmDd",
"caseAnswerFiledDateString",
"caseFilingDateString",
"caseDispositionDateString",
"discoCosDateString",
"discoResponseCosDateString",
}
EXCLUDE_PATTERNS = [
r"^settlementPaymentDate\d{2}$",
r"^settlementPaymentAmount\d{2}$",
r"^settlementRemaingBalance\d{2}$",
r"^settlementRemainingBalance\d{2}$",
r"^debtCollector\d+AccLastFour$",
]
def attrs_from_tag(tag):
return dict(ATTR_RE.findall(tag))
def clean_html_label(value):
value = re.sub(r"<[^>]+>", "", value)
value = value.replace(":", "")
value = re.sub(r"\s+", " ", value).strip()
return value
def nice_label(name):
label = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
label = label.replace("_", " ").replace("-", " ")
label = label.title()
label = label.replace("Ssn", "SSN")
label = label.replace("Dob", "DOB")
label = label.replace("Mm Dd Yyyy", "MM DD YYYY")
return label
def should_exclude(name):
if not name:
return True
if name in EXCLUDE_FIELD_NAMES:
return True
if name.endswith("TemplateFile"):
return True
return any(re.match(pattern, name) for pattern in EXCLUDE_PATTERNS)
def run_node_list_extractor():
js = f"""
import {{ pathToFileURL }} from 'url';
const files = [
'casePlaintiffInfo.js',
'opposingCounselInfo.js',
'judgeInfo.js',
'caseFilingAttorneyInfo.js',
'filingAttorneyInfo.js',
'debtCollectorInfo.js'
];
const base = {json.dumps(str(OLD_PUBLIC))};
const result = {{}};
for (const file of files) {{
try {{
const mod = await import(pathToFileURL(`${{base}}/${{file}}`).href);
for (const [exportName, value] of Object.entries(mod)) {{
if (value && typeof value === 'object' && !Array.isArray(value)) {{
result[exportName] = Object.keys(value).sort();
}}
}}
}} catch (err) {{}}
}}
console.log(JSON.stringify(result));
"""
try:
completed = subprocess.run(
["node", "--input-type=module", "-e", js],
check=True,
capture_output=True,
text=True,
)
return json.loads(completed.stdout)
except Exception:
return {}
def field_type(name, tag_name, attrs):
lower = name.lower()
if tag_name.lower() == "textarea":
return "textarea"
html_type = attrs.get("type", "").lower()
if html_type in {"date", "email", "tel", "number"}:
return html_type
if "date" in lower or lower == "dob":
return "date"
if "email" in lower:
return "email"
if "phone" in lower or "fax" in lower:
return "tel"
if list_name_for_field(name):
return "autocomplete"
return "text"
def list_name_for_field(name):
if name == "casePlaintiff":
return "plaintiffs"
if name == "caseOpposingCounsel":
return "opposingCounsel"
if name == "caseDivisionJudge":
return "judges"
if name == "caseFilingAttorney":
return "filingAttorneys"
if name in {"caseState", "homeState", "client2homeState"}:
return "states"
if name == "caseDesignation":
return "caseDesignations"
if re.fullmatch(r"debtCollector\d+Name", name):
return "debtCollectors"
return None
def section_for(name):
lower = name.lower()
if lower.startswith("client2"):
return "Client 2 Information"
if lower.startswith("client") or lower in {
"ssn", "dob", "alias", "email",
"homeaddress", "homecity", "homestate", "homezip", "homecounty",
"homephone", "cellphone"
}:
return "Client Information"
if lower.startswith("case"):
return "Case Information"
if lower.startswith("disco"):
return "Discovery Information"
if lower.startswith("settlement"):
return "Settlement Information"
if lower.startswith("installment") or lower.startswith("fee") or lower in {
"nameoncard", "cardnumber", "securitycode", "expiration",
"billingaddress", "billingzip"
}:
return "Fee / Payment Information"
if lower.startswith("debtcollector") or name == "numCollectors":
return "Debt Collector Information"
if lower == "notes":
return "Notes"
return "Other Fields"
def discover_templates():
templates = []
for path in sorted(TEMPLATES_OUT.rglob("*.docx")):
rel = path.relative_to(Path("tools/doc_generator/content/templates")).as_posix()
template_id = re.sub(r"[^a-zA-Z0-9]+", "_", path.stem).strip("_").lower()
label = path.relative_to(TEMPLATES_OUT).as_posix()
label = label.replace(".docx", "")
label = label.replace("/", " / ")
label = label.replace("_", " ")
templates.append({
"id": template_id,
"label": label,
"template": rel,
"outputFilename": f"{template_id}_{{caseNumber}}_{{timestamp_YYYY-MM-DD_HH-mm-ss}}.docx"
})
return templates
html = OLD_HTML.read_text(encoding="utf-8", errors="ignore")
labels = {
field_id: clean_html_label(label)
for field_id, label in LABEL_RE.findall(html)
}
fields_seen = []
field_meta = {}
for match in TAG_RE.finditer(html):
tag_name = match.group(1)
tag = match.group(0)
attrs = attrs_from_tag(tag)
name = attrs.get("name") or attrs.get("id")
if should_exclude(name):
continue
if name not in fields_seen:
fields_seen.append(name)
field_meta[name] = (tag_name, attrs)
grouped = {}
for name in fields_seen:
tag_name, attrs = field_meta[name]
ftype = field_type(name, tag_name, attrs)
field = {
"name": name,
"label": labels.get(name) or nice_label(name),
"type": ftype,
"required": False
}
list_name = list_name_for_field(name)
if list_name:
field["list"] = list_name
grouped.setdefault(section_for(name), []).append(field)
preferred_order = [
"Client Information",
"Client 2 Information",
"Case Information",
"Discovery Information",
"Settlement Information",
"Fee / Payment Information",
"Debt Collector Information",
"Notes",
"Other Fields",
]
sections = []
for heading in preferred_order:
fields = grouped.get(heading)
if not fields:
continue
sections.append({
"heading": heading,
"collapsible": heading not in {"Client Information", "Case Information"},
"defaultOpen": heading in {"Client Information", "Case Information"},
"fields": fields
})
lists_raw = run_node_list_extractor()
lists = {
"plaintiffs": lists_raw.get("casePlaintiffInfo", []),
"opposingCounsel": lists_raw.get("caseOpposingCounselInfo", []) or lists_raw.get("opposingCounselInfo", []),
"judges": lists_raw.get("judgeInfo", []),
"filingAttorneys": lists_raw.get("caseFilingAttorneyInfo", []) or lists_raw.get("filingAttorneyInfo", []),
"debtCollectors": lists_raw.get("debtCollectorInfo", []),
"states": ["MO", "KS"],
"caseDesignations": [
"Associate Circuit",
"Circuit",
"Limited Actions",
"Small Claims"
]
}
templates = discover_templates()
profile = {
"id": "legal_profile",
"name": "Legal Profile",
"description": "Consumer debt defense legal profile based on the legacy app form fields. Additional template fields are calculated at generation time.",
"template": templates[0]["template"] if templates else "legacy/Canned-Emails.docx",
"outputFilename": "legal_{caseNumber}_{timestamp_YYYY-MM-DD_HH-mm-ss}.docx",
"lists": lists,
"templates": templates,
"calculations": [
{
"script": "legacy_legal",
"runOn": "generate",
"description": "Generate old-template compatible calculated fields.",
"outputsDynamic": {
"settlementSchedule": {
"countField": "settlementInstallmentNo",
"indexFormat": "decimal2",
"maxCount": 120,
"fields": [
"settlementPaymentDate",
"settlementPaymentAmount",
"settlementRemaingBalance",
"settlementRemainingBalance"
]
}
}
}
],
"sections": sections
}
OUT_PROFILE.write_text(json.dumps(profile, indent=2), encoding="utf-8")
print(f"Wrote {OUT_PROFILE}")
print(f"Visible HTML fields: {len(fields_seen)}")
for section in sections:
print(f"- {section['heading']}: {len(section['fields'])}")

View File

@ -0,0 +1,106 @@
import argparse
import csv
import json
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
MAP_FILE = Path("tools/doc_generator/content/excel_maps/legacy_excel_maps.json")
NS = {
"main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"rel": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
}
def load_map(map_id):
data = json.loads(MAP_FILE.read_text(encoding="utf-8"))
for item in data["maps"]:
if item["id"] == map_id:
return item
raise SystemExit(f"Map not found: {map_id}")
def col_row(cell):
col = "".join(ch for ch in cell if ch.isalpha())
row = "".join(ch for ch in cell if ch.isdigit())
return col, int(row)
def shared_strings(z):
try:
xml = z.read("xl/sharedStrings.xml")
except KeyError:
return []
root = ET.fromstring(xml)
values = []
for si in root.findall("main:si", NS):
parts = []
for t in si.findall(".//main:t", NS):
parts.append(t.text or "")
values.append("".join(parts))
return values
def read_xlsx_cells(path):
values = {}
with zipfile.ZipFile(path) as z:
strings = shared_strings(z)
# MVP: first worksheet only.
sheet_xml = z.read("xl/worksheets/sheet1.xml")
root = ET.fromstring(sheet_xml)
for cell in root.findall(".//main:c", NS):
ref = cell.attrib.get("r")
cell_type = cell.attrib.get("t")
v = cell.find("main:v", NS)
if not ref or v is None:
continue
raw = v.text or ""
if cell_type == "s":
try:
values[ref] = strings[int(raw)]
except Exception:
values[ref] = raw
else:
values[ref] = raw
return values
def export_csv(map_id, xlsx_path, csv_path):
mapping = load_map(map_id)
cells = read_xlsx_cells(xlsx_path)
row = {}
for field, cell in mapping["fields"].items():
row[field] = cells.get(cell, "")
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(mapping["fields"].keys()))
writer.writeheader()
writer.writerow(row)
print(f"Exported {csv_path}")
def main():
parser = argparse.ArgumentParser(description="Export legacy Excel workbook cells to new app CSV datafile.")
parser.add_argument("map_id", help="Map id from legacy_excel_maps.json")
parser.add_argument("xlsx", help="Legacy Excel workbook to read")
parser.add_argument("csv", help="CSV datafile to write")
args = parser.parse_args()
export_csv(args.map_id, Path(args.xlsx), Path(args.csv))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,112 @@
import json
import re
from pathlib import Path
PROFILE = Path("tools/doc_generator/content/document_types/legal_profile.json")
TEMPLATES_ROOT = Path("tools/doc_generator/content/templates")
LEGACY_ROOT = TEMPLATES_ROOT / "legacy"
CATEGORY_RULES = [
("discovery", ["disco", "discovery", "interrog", "request-for-production", "rfp", "admission"]),
("answers", ["answer", "entry-of-appearance"]),
("settlement", ["settlement", "stip", "payment"]),
("client", ["client", "engagement", "fee", "contract"]),
("motions", ["motion", "dismiss", "compel", "summary"]),
("letters", ["letter", "email", "canned"]),
("pleadings", ["petition", "complaint", "counterclaim"]),
]
def title_case(value):
value = value.replace("_", " ").replace("-", " ")
value = re.sub(r"\s+", " ", value).strip()
replacements = {
"disco": "discovery",
"rfp": "request for production",
"cos": "certificate of service",
"oc": "opposing counsel",
"atty": "attorney",
"mo": "Missouri",
"ks": "Kansas",
}
words = []
for word in value.split():
lower = word.lower()
words.append(replacements.get(lower, lower))
return " ".join(words)
def slug(value):
value = title_case(value).lower()
value = re.sub(r"[^a-z0-9]+", "_", value)
return value.strip("_") or "template"
def category_for(relative_path):
text = relative_path.as_posix().lower()
for category, needles in CATEGORY_RULES:
if any(needle in text for needle in needles):
return category
return "general"
def label_for(path):
rel = path.relative_to(LEGACY_ROOT)
parts = list(rel.parts)
parts[-1] = Path(parts[-1]).stem
clean_parts = [title_case(part) for part in parts]
return " / ".join(clean_parts)
def main():
data = json.loads(PROFILE.read_text(encoding="utf-8"))
templates = []
used_ids = set()
for path in sorted(LEGACY_ROOT.rglob("*.docx")):
rel_from_templates = path.relative_to(TEMPLATES_ROOT).as_posix()
rel_from_legacy = path.relative_to(LEGACY_ROOT)
category = category_for(rel_from_legacy)
base_id = f"{category}_{slug(rel_from_legacy.with_suffix('').as_posix())}"
template_id = base_id
n = 2
while template_id in used_ids:
template_id = f"{base_id}_{n}"
n += 1
used_ids.add(template_id)
templates.append({
"id": template_id,
"category": category,
"label": label_for(path),
"template": rel_from_templates,
"outputFilename": f"{template_id}_{{caseNumber}}_{{timestamp_YYYY-MM-DD_HH-mm-ss}}.docx"
})
templates.sort(key=lambda item: (item["category"], item["label"]))
data["templates"] = templates
if templates:
data["defaultTemplateId"] = templates[0]["id"]
data["template"] = templates[0]["template"]
PROFILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
print(f"Updated {PROFILE}")
print(f"Templates: {len(templates)}")
for category in sorted({item["category"] for item in templates}):
count = sum(1 for item in templates if item["category"] == category)
print(f"- {category}: {count}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,251 @@
import json
import re
from pathlib import Path
try:
from docx import Document
except Exception:
Document = None
OLD_APP = Path("/mnt/storage/sftp/mcelwain/repository/word-doc-generator")
OUT_DIR = Path("diagnostics")
OUT_DIR.mkdir(parents=True, exist_ok=True)
PLACEHOLDER_RE = re.compile(r"\{([A-Za-z0-9_:\-]+)\}")
def read_text(path):
try:
return path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
def find_placeholders_in_text(text):
return sorted(set(PLACEHOLDER_RE.findall(text)))
def find_placeholders_in_docx(path):
if Document is None:
return []
found = set()
try:
doc = Document(path)
except Exception:
return []
def scan_paragraphs(paragraphs):
for p in paragraphs:
found.update(find_placeholders_in_text(p.text))
def scan_table(table):
for row in table.rows:
for cell in row.cells:
scan_paragraphs(cell.paragraphs)
for nested in cell.tables:
scan_table(nested)
scan_paragraphs(doc.paragraphs)
for table in doc.tables:
scan_table(table)
return sorted(found)
def categorize_field(name):
lower = name.lower()
if lower.startswith("client2"):
return "Client 2 Information"
if lower.startswith("client") or lower in {"dob", "ssn", "ssnlastfour", "alias", "email"}:
return "Client Information"
if lower.startswith("case"):
return "Case Information"
if lower.startswith("settlement"):
return "Settlement Information"
if lower.startswith("installment") or lower.startswith("fee") or lower in {"nameoncard", "cardnumber", "securitycode", "expiration", "billingaddress", "billingzip"}:
return "Fee / Payment Information"
if lower.startswith("debtcollector"):
return "Debt Collector Information"
if lower.startswith("disco"):
return "Discovery Information"
if lower in {"today", "currentdate", "currentdatemm-dd-yyyy"}:
return "Date Fields"
if lower == "notes":
return "Notes"
return "Other Fields"
def field_type(name):
lower = name.lower()
if "notes" in lower or "appearanceinfo" in lower or "paymentoptions" in lower:
return "textarea"
if "date" in lower or lower in {"dob"}:
return "date"
if "email" in lower:
return "email"
if "phone" in lower or "fax" in lower:
return "tel"
return "text"
def make_sections(fields):
grouped = {}
for name in fields:
grouped.setdefault(categorize_field(name), []).append(name)
preferred_order = [
"Date Fields",
"Client Information",
"Client 2 Information",
"Case Information",
"Discovery Information",
"Settlement Information",
"Fee / Payment Information",
"Debt Collector Information",
"Notes",
"Other Fields",
]
sections = []
for heading in preferred_order:
names = grouped.get(heading)
if not names:
continue
sections.append({
"heading": heading,
"collapsible": heading not in {"Client Information", "Case Information"},
"defaultOpen": heading in {"Client Information", "Case Information"},
"fields": [
{
"name": name,
"label": re.sub(r"([a-z])([A-Z])", r"\1 \2", name).replace("_", " ").strip().title(),
"type": field_type(name),
"required": False
}
for name in sorted(names)
]
})
return sections
js_files = sorted(OLD_APP.rglob("*.js"))
html_files = sorted(OLD_APP.rglob("*.html"))
css_files = sorted(OLD_APP.rglob("*.css"))
docx_files = sorted(OLD_APP.rglob("*.docx"))
xlsx_files = sorted(OLD_APP.rglob("*.xlsx"))
all_text_placeholders = set()
function_hits = []
function_terms = {
"DOCX generation": ["docx", "Docxtemplater", "generateDocument", "generateDoc"],
"Excel generation": ["xlsx", "generateExcel", "template.xlsx"],
"vCard generation": ["vcard", "vCard", "BEGIN:VCARD"],
"Calendar / ICS generation": ["ics", "BEGIN:VCALENDAR", "VEVENT"],
"Client folder generation": ["generateClientFolder", "client folder"],
"Settlement calculations": ["settlementPayment", "settlementInstallment", "remainingBalance"],
}
for path in js_files + html_files:
text = read_text(path)
all_text_placeholders.update(find_placeholders_in_text(text))
for label, terms in function_terms.items():
if any(term in text for term in terms):
function_hits.append((label, str(path.relative_to(OLD_APP))))
template_rows = []
all_template_placeholders = set()
for path in docx_files:
placeholders = find_placeholders_in_docx(path)
all_template_placeholders.update(placeholders)
template_rows.append({
"template": str(path.relative_to(OLD_APP)),
"placeholder_count": len(placeholders),
"placeholders": placeholders,
})
all_fields = sorted(all_text_placeholders | all_template_placeholders)
profile = {
"id": "legacy_word_doc_generator",
"name": "Legacy Word Doc Generator Profile",
"description": "Draft profile generated from the legacy word-doc-generator app.",
"template": "REPLACE_WITH_SELECTED_TEMPLATE.docx",
"outputFilename": "legacy_document_{timestamp_YYYY-MM-DD_HH-mm-ss}.docx",
"sourceApp": str(OLD_APP),
"sections": make_sections(all_fields),
"legacyFeatures": sorted(set(label for label, _ in function_hits)),
"templatesFound": template_rows,
}
profile_path = OUT_DIR / "legacy_word_doc_generator_profile_draft.json"
profile_path.write_text(json.dumps(profile, indent=2), encoding="utf-8")
report = []
report.append("# Legacy Word Doc Generator Review")
report.append("")
report.append(f"Source app: `{OLD_APP}`")
report.append("")
report.append("## Files Found")
report.append("")
report.append(f"- JS files: {len(js_files)}")
report.append(f"- HTML files: {len(html_files)}")
report.append(f"- CSS files: {len(css_files)}")
report.append(f"- DOCX templates: {len(docx_files)}")
report.append(f"- XLSX files: {len(xlsx_files)}")
report.append("")
report.append("## Legacy Features Detected")
report.append("")
if function_hits:
seen = set()
for label, rel in function_hits:
key = (label, rel)
if key in seen:
continue
seen.add(key)
report.append(f"- {label}: `{rel}`")
else:
report.append("- No major legacy feature signatures detected.")
report.append("")
report.append("## Templates Found")
report.append("")
if template_rows:
for row in template_rows:
report.append(f"### `{row['template']}`")
report.append(f"- Placeholder count: {row['placeholder_count']}")
if row["placeholders"]:
report.append("- Placeholders:")
for name in row["placeholders"]:
report.append(f" - `{{{name}}}`")
report.append("")
else:
report.append("- No DOCX templates found.")
report.append("")
report.append("## All Fields Detected")
report.append("")
for name in all_fields:
report.append(f"- `{{{name}}}`")
report.append("")
report.append("## Draft Profile")
report.append("")
report.append(f"Generated: `{profile_path}`")
report.append("")
report_path = OUT_DIR / "legacy_word_doc_generator_review.md"
report_path.write_text("\n".join(report), encoding="utf-8")
print(f"Wrote {report_path}")
print(f"Wrote {profile_path}")
print(f"Detected {len(all_fields)} unique fields/placeholders")

View File

@ -10,6 +10,8 @@ const downloadDataButton = document.getElementById("downloadDataButton");
const downloadJsonButton = document.getElementById("downloadJsonButton");
const defaultDocumentOptions = document.getElementById("defaultDocumentOptions");
const uploadedJsonOptions = document.getElementById("uploadedJsonOptions");
const legacyTemplateSelect = document.getElementById("legacyTemplateSelect");
const legacyTemplateRow = document.getElementById("legacyTemplateRow");
const documentPickerToggle = document.getElementById("documentPickerToggle");
const documentPickerMenu = document.getElementById("documentPickerMenu");
const selectedDocumentLabel = document.getElementById("selectedDocumentLabel");
@ -19,6 +21,7 @@ let currentFields = [];
let defaultDocumentTypes = [];
let activePickerKind = "default";
let activePickerId = "";
let selectedTemplateId = "";
function setStatus(message) {
statusBox.innerHTML = message || "";
@ -67,7 +70,13 @@ function createInput(field) {
}
} else {
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;
@ -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) {
fieldsContainer.innerHTML = "";
currentFields = [];
renderDatalists(documentType);
renderTemplateSelector(documentType);
if (Array.isArray(documentType.sections)) {
for (const section of documentType.sections) {
@ -600,6 +669,7 @@ docForm.addEventListener("submit", async event => {
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
document_type_id: currentDocumentType.id,
template_id: getSelectedTemplateId(),
data: getFormData(true)
})
});
@ -750,3 +820,552 @@ if (savedActiveView) {
requestAnimationFrame(() => {
document.body.classList.remove("app-loading");
});
if (legacyTemplateSelect) {
legacyTemplateSelect.addEventListener("change", () => {
selectedTemplateId = legacyTemplateSelect.value;
if (currentDocumentType?.id) {
localStorage.setItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`, selectedTemplateId);
}
saveCurrentFormData();
});
}
function removeDynamicFields(groupId) {
document.querySelectorAll(`[data-dynamic-group="${groupId}"]`).forEach(el => el.remove());
currentFields = currentFields.filter(field => field.dynamicGroup !== groupId);
}
function renderDynamicFieldGroup(group) {
if (!currentDocumentType) return;
const countEl = document.getElementById(group.countField);
if (!countEl) return;
const count = Math.min(
parseInt(countEl.value || "0", 10) || 0,
group.maxCount || 100
);
removeDynamicFields(group.id);
if (count <= 0) return;
const wrapper = document.createElement("div");
wrapper.dataset.dynamicGroup = group.id;
wrapper.className = "json-section dynamic-field-group";
const details = document.createElement("details");
details.open = group.defaultOpen !== false;
const summary = document.createElement("summary");
summary.textContent = `${group.section || "Dynamic Fields"} (${count})`;
details.appendChild(summary);
const generatedFields = [];
for (let n = 1; n <= count; n++) {
for (const fieldDef of group.fields || []) {
generatedFields.push({
name: fieldDef.namePattern.replaceAll("{n}", String(n)),
label: fieldDef.labelPattern.replaceAll("{n}", String(n)),
type: fieldDef.type || "text",
list: fieldDef.list,
required: Boolean(fieldDef.required),
dynamicGroup: group.id
});
}
}
currentFields.push(...generatedFields);
appendFieldRows(details, generatedFields);
wrapper.appendChild(details);
const countField = document.getElementById(group.countField);
const sectionWrapper = countField?.closest(".json-section");
if (sectionWrapper) {
sectionWrapper.appendChild(wrapper);
} else {
fieldsContainer.appendChild(wrapper);
}
restoreSavedFormData();
}
function renderAllDynamicFieldGroups() {
if (!currentDocumentType?.dynamicFieldGroups) return;
for (const group of currentDocumentType.dynamicFieldGroups) {
renderDynamicFieldGroup(group);
const countEl = document.getElementById(group.countField);
if (countEl && !countEl.dataset.dynamicListenerAttached) {
countEl.dataset.dynamicListenerAttached = "true";
countEl.addEventListener("input", () => {
saveCurrentFormData();
renderDynamicFieldGroup(group);
});
countEl.addEventListener("change", () => {
saveCurrentFormData();
renderDynamicFieldGroup(group);
});
}
}
}
const originalRenderDocumentTypeForDynamicGroups = renderDocumentType;
renderDocumentType = function(documentType) {
originalRenderDocumentTypeForDynamicGroups(documentType);
renderAllDynamicFieldGroups();
loadExcelMaps();
};
const templateFileInput = document.getElementById("templateFileInput");
async function fetchUploadedTemplatesForCurrentProfile() {
if (!currentDocumentType?.id) return [];
try {
const response = await fetch(`/api/doc-generator/uploaded-templates/${encodeURIComponent(currentDocumentType.id)}`);
const json = await response.json();
if (!response.ok) {
return [];
}
return json.templates || [];
} catch {
return [];
}
}
const originalRenderTemplateSelectorWithUploads = renderTemplateSelector;
renderTemplateSelector = function(documentType) {
if (!legacyTemplateSelect || !legacyTemplateRow) return;
const libraryTemplates = documentType.templates || [];
legacyTemplateSelect.innerHTML = "";
if (!libraryTemplates.length && !documentType.id) {
legacyTemplateRow.style.display = "none";
selectedTemplateId = "";
return;
}
legacyTemplateRow.style.display = "block";
if (libraryTemplates.length) {
const libraryGroup = document.createElement("optgroup");
libraryGroup.label = "Library Templates";
for (const template of libraryTemplates) {
const option = document.createElement("option");
option.value = template.id;
option.textContent = template.label || template.id;
libraryGroup.appendChild(option);
}
legacyTemplateSelect.appendChild(libraryGroup);
}
const uploadedGroup = document.createElement("optgroup");
uploadedGroup.label = "Uploaded Templates";
uploadedGroup.id = "uploadedTemplateOptGroup";
legacyTemplateSelect.appendChild(uploadedGroup);
const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${documentType.id}`);
selectedTemplateId = savedTemplate || documentType.defaultTemplateId || libraryTemplates[0]?.id || "";
if (selectedTemplateId) {
legacyTemplateSelect.value = selectedTemplateId;
}
refreshUploadedTemplateOptions();
};
async function refreshUploadedTemplateOptions() {
if (!legacyTemplateSelect || !currentDocumentType?.id) return;
let uploadedGroup = document.getElementById("uploadedTemplateOptGroup");
if (!uploadedGroup) {
uploadedGroup = document.createElement("optgroup");
uploadedGroup.label = "Uploaded Templates";
uploadedGroup.id = "uploadedTemplateOptGroup";
legacyTemplateSelect.appendChild(uploadedGroup);
}
uploadedGroup.innerHTML = "";
const uploadedTemplates = await fetchUploadedTemplatesForCurrentProfile();
for (const template of uploadedTemplates) {
const option = document.createElement("option");
option.value = template.id;
option.textContent = template.label || template.filename;
uploadedGroup.appendChild(option);
}
const savedTemplate = localStorage.getItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`);
if (savedTemplate && [...legacyTemplateSelect.options].some(option => option.value === savedTemplate)) {
legacyTemplateSelect.value = savedTemplate;
selectedTemplateId = savedTemplate;
}
}
if (templateFileInput) {
templateFileInput.addEventListener("change", async event => {
const file = event.target.files[0];
if (!file) return;
if (!currentDocumentType?.id) {
setStatus("Select a profile before uploading a template.");
templateFileInput.value = "";
return;
}
try {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(`/api/doc-generator/upload-template/${encodeURIComponent(currentDocumentType.id)}`, {
method: "POST",
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.detail || "Template upload failed.");
}
await refreshUploadedTemplateOptions();
legacyTemplateSelect.value = result.template_id;
selectedTemplateId = result.template_id;
localStorage.setItem(`utilityAppSelectedTemplate:${currentDocumentType.id}`, selectedTemplateId);
setStatus(`Uploaded template: ${result.filename}<br>Saved to: ${result.saved_path}`);
} catch (error) {
setStatus(`Could not upload template: ${error.message}`);
} finally {
templateFileInput.value = "";
}
});
}
const excelMapSelect = document.getElementById("excelMapSelect");
const exportExcelButton = document.getElementById("exportExcelButton");
const excelImportInput = document.getElementById("excelImportInput");
async function loadExcelMaps() {
if (!excelMapSelect) return;
try {
if (!currentDocumentType?.id) return;
const response = await fetch(`/api/doc-generator/excel-maps/${encodeURIComponent(currentDocumentType.id)}`);
const json = await response.json();
if (!response.ok) {
throw new Error(json.detail || "Could not load Excel maps.");
}
excelMapSelect.innerHTML = "";
for (const item of json.maps || []) {
const option = document.createElement("option");
option.value = item.id;
option.textContent = `${item.label || item.id} (${item.field_count} fields)`;
excelMapSelect.appendChild(option);
}
const saved = localStorage.getItem("utilityAppExcelMapId");
if (saved && [...excelMapSelect.options].some(option => option.value === saved)) {
excelMapSelect.value = saved;
}
} catch (error) {
setStatus(`Could not load Excel maps: ${error.message}`);
}
}
if (excelMapSelect) {
excelMapSelect.addEventListener("change", () => {
localStorage.setItem("utilityAppExcelMapId", excelMapSelect.value);
});
}
if (exportExcelButton) {
exportExcelButton.addEventListener("click", async () => {
if (!currentDocumentType?.id) {
setStatus("Select a profile before exporting Excel.");
return;
}
try {
saveCurrentFormData();
const response = await fetch("/api/doc-generator/export-excel", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
document_type_id: currentDocumentType.id,
map_id: excelMapSelect?.value || "field_to_cell_map",
data: getFormData(true)
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.detail || "Excel export failed.");
}
const link = document.createElement("a");
link.href = result.download_url;
link.download = result.filename;
document.body.appendChild(link);
link.click();
link.remove();
setStatus(`Exported Excel: ${result.filename}`);
} catch (error) {
setStatus(`Could not export Excel: ${error.message}`);
}
});
}
if (excelImportInput) {
excelImportInput.addEventListener("change", () => {
setStatus("Excel import is not wired yet. Export is ready.");
excelImportInput.value = "";
});
}
loadExcelMaps();
/* Excel template download button */
const downloadExcelTemplateButton = document.getElementById("downloadExcelTemplateButton");
if (downloadExcelTemplateButton) {
downloadExcelTemplateButton.addEventListener("click", () => {
if (!currentDocumentType?.id || !excelMapSelect?.value) {
setStatus("Select a profile and Excel template/map first.");
return;
}
window.location.href = `/api/doc-generator/excel-template-by-map/${encodeURIComponent(currentDocumentType.id)}/${encodeURIComponent(excelMapSelect.value)}`;
});
}
/* final legal profile UI normalization */
function legalProfileDisplayName(label) {
const value = String(label || "").trim();
if (value === "Legal Profile" || value === "legal_profile") return "Legal";
return value;
}
function normalizeLegalUi() {
const selectedLabel = document.getElementById("selectedDocumentLabel");
if (selectedLabel) {
selectedLabel.textContent = legalProfileDisplayName(selectedLabel.textContent);
}
const description = document.getElementById("documentDescription");
if (description && currentDocumentType?.id === "legal_profile") {
description.textContent = "Consumer Debt Defense Legal Profile";
description.classList.add("legal-profile-caption");
}
setupSelectPicker("excelMapSelect", "Excel Template / Map");
setupSelectPicker("legacyTemplateSelect", "Document Template");
renderUploadedTemplateDeleteUi().catch(() => {});
}
function setupSelectPicker(selectId, fallbackLabel) {
const select = document.getElementById(selectId);
if (!select) return;
select.classList.add("native-select-hidden");
let picker = document.querySelector(`[data-select-picker-for="${selectId}"]`);
if (!picker) {
picker = document.createElement("div");
picker.className = "document-picker dropdown-picker select-picker";
picker.dataset.selectPickerFor = selectId;
picker.innerHTML = `
<button class="document-picker-toggle select-picker-toggle" type="button">
<span class="select-picker-label">${fallbackLabel}</span>
<span class="dropdown-arrow"></span>
</button>
<div class="document-picker-menu select-picker-menu"></div>
`;
select.insertAdjacentElement("afterend", picker);
const toggle = picker.querySelector(".select-picker-toggle");
toggle.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
document.querySelectorAll(".select-picker.open").forEach(openPicker => {
if (openPicker !== picker) openPicker.classList.remove("open");
});
picker.classList.toggle("open");
});
}
const menu = picker.querySelector(".select-picker-menu");
const label = picker.querySelector(".select-picker-label");
menu.innerHTML = "";
const options = [...select.options].filter(option => option.value !== "");
if (!options.length) {
label.textContent = fallbackLabel;
return;
}
const selectedOption = select.selectedOptions?.[0] || options[0];
label.textContent = selectedOption?.textContent?.trim() || fallbackLabel;
for (const option of options) {
const button = document.createElement("button");
button.type = "button";
button.className = "picker-item";
if (option.value === select.value) button.classList.add("active");
button.textContent = option.textContent.trim();
button.addEventListener("click", event => {
event.preventDefault();
select.value = option.value;
select.dispatchEvent(new Event("change", { bubbles: true }));
picker.classList.remove("open");
setupSelectPicker(selectId, fallbackLabel);
});
menu.appendChild(button);
}
}
document.addEventListener("click", () => {
document.querySelectorAll(".select-picker.open").forEach(picker => picker.classList.remove("open"));
});
const originalSetActivePickerForLegalUi = typeof setActivePicker === "function" ? setActivePicker : null;
if (originalSetActivePickerForLegalUi) {
setActivePicker = function(kind, id, label) {
originalSetActivePickerForLegalUi(kind, id, legalProfileDisplayName(label));
normalizeLegalUi();
};
}
const originalRenderDocumentTypeForLegalUi = renderDocumentType;
renderDocumentType = function(documentType) {
originalRenderDocumentTypeForLegalUi(documentType);
normalizeLegalUi();
};
const originalRenderTemplateSelectorForLegalUi = renderTemplateSelector;
renderTemplateSelector = function(documentType) {
originalRenderTemplateSelectorForLegalUi(documentType);
normalizeLegalUi();
};
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 template of uploaded) {
const id = template.id || template.filename;
const name = template.label || template.filename || id;
const row = document.createElement("div");
row.className = "uploaded-template-row";
const nameSpan = document.createElement("span");
nameSpan.textContent = name;
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-json-button";
deleteButton.textContent = "x";
deleteButton.title = `Delete ${name}`;
deleteButton.addEventListener("click", async event => {
event.preventDefault();
event.stopPropagation();
if (!confirm(`Delete uploaded template?\n\n${name}`)) return;
const deleteId = String(id).replace(/^uploaded:/, "");
const response = await fetch(`/api/doc-generator/uploaded-template/${encodeURIComponent(currentDocumentType.id)}/${encodeURIComponent(deleteId)}`, {
method: "DELETE"
});
if (!response.ok) {
setStatus("Unable to delete uploaded template.");
return;
}
setStatus("Uploaded template deleted.");
await refreshUploadedTemplatesForCurrentProfile();
normalizeLegalUi();
});
row.appendChild(nameSpan);
row.appendChild(deleteButton);
list.appendChild(row);
}
}
setInterval(() => {
normalizeLegalUi();
}, 1000);
normalizeLegalUi();

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>Utility App</title>
<link rel="stylesheet" href="/static/styles.css?v=shell1">
<link rel="stylesheet" href="/static/styles.css?v=legalui4">
</head>
<body class="app-loading">
<header class="app-header">
@ -55,6 +55,11 @@
</label>
<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>
<h2>Document Type</h2>
@ -78,7 +83,44 @@
<div id="documentDescription"></div>
</div>
<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>
<h2>Generate Documents</h2>
@ -110,6 +152,6 @@
<div id="status"></div>
</main>
<script src="/static/app.js?v=shell1"></script>
<script src="/static/app.js?v=legalui4"></script>
</body>
</html>

View File

@ -429,3 +429,516 @@ body.app-loading .container {
.container {
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;
}
}
/* Legal profile dropdown normalization */
#documentDescription.legal-profile-caption,
.legal-profile-caption {
margin-top: 0.35rem;
color: #8a8f98;
font-size: 0.88rem;
font-weight: 400;
}
.native-select-hidden {
display: none !important;
}
.select-picker {
width: 100%;
margin-top: 0.35rem;
}
.select-picker .document-picker-toggle {
width: 100%;
}
.select-picker-menu {
max-height: 18rem;
overflow-y: auto;
}
.uploaded-template-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-top: 0.4rem;
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}
]
}

Some files were not shown because too many files have changed in this diff Show More