Compare commits
3 Commits
main
...
work-phone
| Author | SHA1 | Date |
|---|---|---|
|
|
deea989f77 | |
|
|
adcba89350 | |
|
|
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.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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ uvicorn[standard]
|
|||
python-multipart
|
||||
python-docx
|
||||
pendulum
|
||||
openpyxl
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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'])}")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
621
static/app.js
621
static/app.js
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tools/doc_generator/content/templates/legacy/Discovery/BlittKS-disco - Copy.docx
Executable file
BIN
tools/doc_generator/content/templates/legacy/Discovery/BlittKS-disco - Copy.docx
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tools/doc_generator/content/templates/legacy/Discovery/Burns-Walsh-KS-DB-CC.docx
Executable file
BIN
tools/doc_generator/content/templates/legacy/Discovery/Burns-Walsh-KS-DB-CC.docx
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tools/doc_generator/content/templates/legacy/Discovery/Southlaw-MO-OC-disco.docx
Executable file
BIN
tools/doc_generator/content/templates/legacy/Discovery/Southlaw-MO-OC-disco.docx
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue