This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

View File

@@ -56,6 +56,30 @@
<i class="fa-solid fa-circle-question"></i>
Help
</button>
<div class="hidden md:flex items-center gap-2 ml-2">
<label class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Templates:</label>
<button type="button" id="downloadFilesTemplateBtn" class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Download FILES.csv template">
FILES
</button>
<button type="button" id="downloadLedgerTemplateBtn" class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Download LEDGER.csv template">
LEDGER
</button>
<button type="button" id="downloadPaymentsTemplateBtn" class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Download PAYMENTS.csv template">
PAYMENTS
</button>
<button type="button" id="downloadRolodexTemplateBtn" class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Download ROLODEX.csv template">
ROLODEX
</button>
<button type="button" id="downloadTrnsactnTemplateBtn" class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Download TRNSACTN.csv template">
TRNSACTN
</button>
<button type="button" id="downloadDepositsTemplateBtn" class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Download DEPOSITS.csv template">
DEPOSITS
</button>
<button type="button" id="downloadTemplatesBundleBtn" class="px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700" title="Download selected templates as ZIP">
Download…
</button>
</div>
</div>
</div>
</div>
@@ -166,11 +190,15 @@
<h5 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<i class="fa-solid fa-clipboard-check"></i>
<span>File Validation Results</span>
<button type="button" class="ml-auto text-xs underline text-neutral-600 dark:text-neutral-400" onclick="toggleHeaderHelp()">Required headers help</button>
</h5>
</div>
<div class="p-6" id="validationResults">
<!-- Validation results will be shown here -->
</div>
<div class="px-6 pb-4 hidden" id="headerHelp">
<!-- Content will be populated dynamically by toggleHeaderHelp() -->
</div>
</div>
<!-- Import Progress Panel -->
@@ -273,6 +301,94 @@
</div>
<script>
// Centralized required headers map with examples
const CSV_REQUIRED_HEADERS = {
'FILES.csv': {
required: ['file_no', 'id', 'empl_num', 'file_type', 'opened', 'status', 'rate_per_hour'],
example: 'File_No,Id,Empl_Num,File_Type,Opened,Status,Rate_Per_Hour\nF-001,CLIENT-1,EMP01,CIVIL,2024-01-01,ACTIVE,150'
},
'LEDGER.csv': {
required: ['file_no', 'date', 'empl_num', 't_code', 't_type', 'amount'],
example: 'File_No,Date,Empl_Num,T_Code,T_Type,Amount\nF-001,2024-01-15,EMP01,FEE,1,500.00'
},
'PAYMENTS.csv': {
required: ['deposit_date', 'amount'],
example: 'Deposit_Date,Amount\n2024-01-15,1500.00'
},
'TRNSACTN.csv': {
required: ['file_no', 'date', 'empl_num', 't_code', 't_type', 'amount'],
example: 'File_No,Date,Empl_Num,T_Code,T_Type,Amount\nF-002,2024-02-10,EMP02,FEE,1,250.00'
},
'DEPOSITS.csv': {
required: ['deposit_date', 'total'],
example: 'Deposit_Date,Total\n2024-02-10,1500.00'
},
'ROLODEX.csv': {
required: ['id', 'last'],
example: 'Id,Last,First,A1,City,Abrev,Zip,Email\nCLIENT-1,Smith,John,123 Main St,Denver,CO,80202,john.smith@example.com'
}
};
function getRequiredHeadersText(fileType) {
const info = CSV_REQUIRED_HEADERS[fileType];
return info ? info.required.join(', ') : 'varies';
}
function getRequiredHeadersTooltip(fileType) {
const info = CSV_REQUIRED_HEADERS[fileType];
if (!info) return 'Required headers vary by file type';
return `Required: ${info.required.join(', ')}\n\nExample:\n${info.example}`;
}
function toggleHeaderHelp() {
const el = document.getElementById('headerHelp');
if (!el) return;
// Update content dynamically if not already populated
if (!el.dataset.populated) {
const content = `
<div class="p-3 bg-neutral-50 dark:bg-neutral-900 rounded text-xs text-neutral-700 dark:text-neutral-300">
<div class="font-semibold mb-2">Minimal required headers by CSV:</div>
<div class="space-y-3">
<div>
<div class="font-mono font-semibold">FILES.csv</div>
<div class="text-neutral-600 dark:text-neutral-400">Required: ${getRequiredHeadersText('FILES.csv')}</div>
<pre class="font-mono text-xs mt-1 text-neutral-500 whitespace-pre-wrap">${CSV_REQUIRED_HEADERS['FILES.csv'].example}</pre>
</div>
<div>
<div class="font-mono font-semibold">LEDGER.csv</div>
<div class="text-neutral-600 dark:text-neutral-400">Required: ${getRequiredHeadersText('LEDGER.csv')}</div>
<pre class="font-mono text-xs mt-1 text-neutral-500 whitespace-pre-wrap">${CSV_REQUIRED_HEADERS['LEDGER.csv'].example}</pre>
</div>
<div>
<div class="font-mono font-semibold">PAYMENTS.csv</div>
<div class="text-neutral-600 dark:text-neutral-400">Required: ${getRequiredHeadersText('PAYMENTS.csv')}</div>
<pre class="font-mono text-xs mt-1 text-neutral-500 whitespace-pre-wrap">${CSV_REQUIRED_HEADERS['PAYMENTS.csv'].example}</pre>
</div>
<div>
<div class="font-mono font-semibold">TRNSACTN.csv</div>
<div class="text-neutral-600 dark:text-neutral-400">Required: ${getRequiredHeadersText('TRNSACTN.csv')}</div>
<pre class="font-mono text-xs mt-1 text-neutral-500 whitespace-pre-wrap">${CSV_REQUIRED_HEADERS['TRNSACTN.csv'].example}</pre>
</div>
<div>
<div class="font-mono font-semibold">DEPOSITS.csv</div>
<div class="text-neutral-600 dark:text-neutral-400">Required: ${getRequiredHeadersText('DEPOSITS.csv')}</div>
<pre class="font-mono text-xs mt-1 text-neutral-500 whitespace-pre-wrap">${CSV_REQUIRED_HEADERS['DEPOSITS.csv'].example}</pre>
</div>
<div>
<div class="font-mono font-semibold">ROLODEX.csv</div>
<div class="text-neutral-600 dark:text-neutral-400">Required: ${getRequiredHeadersText('ROLODEX.csv')}</div>
<pre class="font-mono text-xs mt-1 text-neutral-500 whitespace-pre-wrap">${CSV_REQUIRED_HEADERS['ROLODEX.csv'].example}</pre>
</div>
</div>
</div>
`;
el.innerHTML = content;
el.dataset.populated = 'true';
}
el.classList.toggle('hidden');
}
// Import functionality
let availableFiles = {};
let importInProgress = false;
@@ -328,6 +444,20 @@ function setupEventListeners() {
document.getElementById('uploadMode').addEventListener('change', switchUploadMode);
const helpBtn = document.getElementById('importHelpBtn');
if (helpBtn) helpBtn.addEventListener('click', showImportHelp);
const tfBtn = document.getElementById('downloadFilesTemplateBtn');
const tlBtn = document.getElementById('downloadLedgerTemplateBtn');
const tpBtn = document.getElementById('downloadPaymentsTemplateBtn');
const trBtn = document.getElementById('downloadRolodexTemplateBtn');
const ttBtn = document.getElementById('downloadTrnsactnTemplateBtn');
const tdBtn = document.getElementById('downloadDepositsTemplateBtn');
const tbBtn = document.getElementById('downloadTemplatesBundleBtn');
if (tfBtn) tfBtn.addEventListener('click', () => downloadTemplateFor('FILES.csv'));
if (tlBtn) tlBtn.addEventListener('click', () => downloadTemplateFor('LEDGER.csv'));
if (tpBtn) tpBtn.addEventListener('click', () => downloadTemplateFor('PAYMENTS.csv'));
if (trBtn) trBtn.addEventListener('click', () => downloadTemplateFor('ROLODEX.csv'));
if (ttBtn) ttBtn.addEventListener('click', () => downloadTemplateFor('TRNSACTN.csv'));
if (tdBtn) tdBtn.addEventListener('click', () => downloadTemplateFor('DEPOSITS.csv'));
if (tbBtn) tbBtn.addEventListener('click', openTemplateBundleDialog);
// Validation buttons
document.getElementById('validateBtn').addEventListener('click', validateFile);
@@ -818,7 +948,7 @@ function showImportHelp() {
</div>
<div>
Files will be imported in dependency order automatically:
<code class="block mt-1">STATES.csv → GRUPLKUP.csv → EMPLOYEE.csv → FILETYPE.csv → FILESTAT.csv → TRNSTYPE.csv → TRNSLKUP.csv → FOOTERS.csv → SETUP.csv → PRINTERS.csv → ROLODEX.csv → PHONE.csv → FILES.csv → LEDGER.csv → TRNSACTN.csv → QDROS.csv → PENSIONS.csv → PLANINFO.csv → PAYMENTS.csv → DEPOSITS.csv → FILENOTS.csv → FORM_INX.csv → FORM_LST.csv → FVARLKUP.csv → RVARLKUP.csv</code>
<code class="block mt-1">STATES.csv → GRUPLKUP.csv → EMPLOYEE.csv → FILETYPE.csv → FOOTERS.csv → FILESTAT.csv → TRNSTYPE.csv → TRNSLKUP.csv → SETUP.csv → PRINTERS.csv → ROLODEX.csv → PHONE.csv → FILES.csv → LEDGER.csv → TRNSACTN.csv → QDROS.csv → PENSIONS.csv → PLANINFO.csv → PAYMENTS.csv → DEPOSITS.csv → FILENOTS.csv → FORM_INX.csv → FORM_LST.csv → FVARLKUP.csv → RVARLKUP.csv</code>
</div>
<div>
Unrecognized columns are saved as flexible JSON automatically. Unknown CSVs fall back to flexible-only storage.
@@ -837,6 +967,104 @@ function showImportHelp() {
}
}
async function downloadTemplateFor(type) {
try {
const resp = await window.http.wrappedFetch(`/api/import/template/${encodeURIComponent(type)}`);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || `Failed to download template for ${type}`);
}
const blob = await resp.blob();
let filename = `${(type || '').replace('.csv','')}_template.csv`;
const cd = resp.headers.get('content-disposition') || '';
const m = cd.match(/filename="?([^";]+)"?/i);
if (m && m[1]) filename = m[1];
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
a.remove();
} catch (error) {
showAlert('Template download failed: ' + (error?.message || 'Unknown error'), 'danger');
}
}
function openTemplateBundleDialog() {
const content = `
<div class="text-sm">
<div class="mb-2">Select CSV templates to include:</div>
<div class="space-y-2">
<label class="flex items-center gap-2"><input type="checkbox" name="tpl" value="FILES.csv" checked> <span>FILES.csv</span></label>
<label class="flex items-center gap-2"><input type="checkbox" name="tpl" value="LEDGER.csv" checked> <span>LEDGER.csv</span></label>
<label class="flex items-center gap-2"><input type="checkbox" name="tpl" value="PAYMENTS.csv" checked> <span>PAYMENTS.csv</span></label>
<label class="flex items-center gap-2"><input type="checkbox" name="tpl" value="TRNSACTN.csv" checked> <span>TRNSACTN.csv</span></label>
<label class="flex items-center gap-2"><input type="checkbox" name="tpl" value="DEPOSITS.csv" checked> <span>DEPOSITS.csv</span></label>
<label class="flex items-center gap-2"><input type="checkbox" name="tpl" value="ROLODEX.csv" checked> <span>ROLODEX.csv</span></label>
</div>
<div class="mt-3 text-xs text-neutral-500">A ZIP will be generated containing minimal templates with required headers and a sample row.</div>
</div>
`;
if (window.alerts && window.alerts.show) {
window.alerts.show(content, 'info', {
html: true,
duration: 0,
title: 'Download Templates',
actions: [
{
label: 'Download ZIP',
classes: 'px-3 py-1 rounded text-xs bg-primary-600 text-white hover:bg-primary-700',
onClick: async ({ wrapper }) => {
const inputs = wrapper.querySelectorAll('input[name="tpl"]:checked');
const files = Array.from(inputs).map(i => i.value);
if (!files.length) {
showAlert('Please select at least one template', 'warning');
return;
}
await downloadTemplatesBundle(files);
}
},
{
label: 'Cancel',
classes: 'px-3 py-1 rounded text-xs bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200',
autoClose: true
}
]
});
} else {
showAlert('Template selection requires alerts UI. Please download single templates.', 'info');
}
}
async function downloadTemplatesBundle(files) {
try {
const params = new URLSearchParams();
for (const f of files) params.append('files', f);
const resp = await window.http.wrappedFetch(`/api/import/templates/bundle?${params.toString()}`);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || 'Failed to download templates bundle');
}
const blob = await resp.blob();
let filename = 'csv_templates.zip';
const cd = resp.headers.get('content-disposition') || '';
const m = cd.match(/filename="?([^";]+)"?/i);
if (m && m[1]) filename = m[1];
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
a.remove();
} catch (error) {
showAlert('Bundle download failed: ' + (error?.message || 'Unknown error'), 'danger');
}
}
async function validateAllFiles() {
const fileInput = document.getElementById('batchFiles');
@@ -936,7 +1164,9 @@ function displayBatchValidationResults(results, allValid) {
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<i class="fa-solid fa-${resultIcon} text-${resultClass}-600 dark:text-${resultClass}-400"></i>
<strong class="text-sm">${result.file_type}</strong>
<strong class="text-sm flex items-center gap-2">${result.file_type}
<i class="fa-solid fa-circle-info text-neutral-500" title="${getRequiredHeadersTooltip(result.file_type)}"></i>
</strong>
</div>
<div class="text-sm text-${resultClass}-600 dark:text-${resultClass}-400 capitalize">${status}</div>
</div>
@@ -948,6 +1178,11 @@ function displayBatchValidationResults(results, allValid) {
html += `<p class="text-xs text-neutral-600 dark:text-neutral-400 mt-1">${mappedCount} mapped, ${unmappedCount} unmapped</p>`;
}
if (result.header_validation && result.header_validation.ok === false) {
const missing = (result.header_validation.missing_fields || []).join(', ');
html += `<p class="text-xs text-danger-600 dark:text-danger-400 mt-1">Missing required headers: ${missing || 'unknown'}</p>`;
}
if (result.total_errors > 0) {
html += `<p class="text-xs text-neutral-600 dark:text-neutral-400 mt-1">${result.total_errors} data validation errors found</p>`;
}
@@ -1000,8 +1235,8 @@ function updateSelectedFiles() {
if (files.length > 0) {
// Define import order
const importOrder = [
"STATES.csv", "GRUPLKUP.csv", "EMPLOYEE.csv", "FILETYPE.csv", "FILESTAT.csv",
"TRNSTYPE.csv", "TRNSLKUP.csv", "FOOTERS.csv", "SETUP.csv", "PRINTERS.csv",
"STATES.csv", "GRUPLKUP.csv", "EMPLOYEE.csv", "FILETYPE.csv", "FOOTERS.csv", "FILESTAT.csv",
"TRNSTYPE.csv", "TRNSLKUP.csv", "SETUP.csv", "PRINTERS.csv",
"ROLODEX.csv", "PHONE.csv", "FILES.csv", "LEDGER.csv", "TRNSACTN.csv",
"QDROS.csv", "PENSIONS.csv", "PLANINFO.csv", "PAYMENTS.csv", "DEPOSITS.csv",
"FILENOTS.csv", "FORM_INX.csv", "FORM_LST.csv", "FVARLKUP.csv", "RVARLKUP.csv"
@@ -1238,7 +1473,9 @@ function displayBatchResults(result) {
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<i class="fa-solid fa-${statusIcon} text-${statusClass}-600 dark:text-${statusClass}-400"></i>
<strong class="text-sm">${fileResult.file_type}</strong>
<strong class="text-sm flex items-center gap-2">${fileResult.file_type}
<i class="fa-solid fa-circle-info text-neutral-500" title="${getRequiredHeadersTooltip(fileResult.file_type)}"></i>
</strong>
</div>
<div class="text-right text-sm">
${fileResult.imported_count ? `<span class="text-success-600 dark:text-success-400">${fileResult.imported_count} imported</span>` : ''}
@@ -1252,6 +1489,11 @@ function displayBatchResults(result) {
<span class=\"ml-2\">${(fileResult.auto_mapping.unmapped_headers || []).length} unmapped (stored as flexible)</span>
</div>
` : ''}
${(fileResult.header_validation && fileResult.header_validation.ok === false) ? `
<div class=\"mt-2 text-xs text-danger-600 dark:text-danger-400\">
Missing required headers: ${(fileResult.header_validation.missing_fields || []).join(', ') || 'unknown'}
</div>
` : ''}
</div>
`;
});
@@ -1369,15 +1611,20 @@ async function viewAuditDetails(auditId) {
const resp = await window.http.wrappedFetch(`/api/import/recent-batches/${auditId}`);
if (!resp.ok) return;
const data = await resp.json();
const files = (data.files || []).map(f => `
<tr>
<td class="px-3 py-2 text-sm font-mono">${f.file_type}</td>
<td class="px-3 py-2 text-sm"><span class="inline-block px-2 py-0.5 rounded ${f.status === 'success' ? 'bg-green-100 text-green-700' : (f.status === 'completed_with_errors' ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700')}">${f.status}</span></td>
<td class="px-3 py-2 text-sm">${f.imported_count}</td>
<td class="px-3 py-2 text-sm">${f.errors}</td>
<td class="px-3 py-2 text-sm">${f.message || ''}</td>
</tr>
`).join('');
const files = (data.files || []).map(f => {
const hv = (f.details && f.details.header_validation) ? f.details.header_validation : null;
const hvCell = hv && hv.ok === false ? `Missing: ${(hv.missing_fields || []).join(', ')}` : '—';
return `
<tr>
<td class="px-3 py-2 text-sm font-mono">${f.file_type}</td>
<td class="px-3 py-2 text-sm"><span class="inline-block px-2 py-0.5 rounded ${f.status === 'success' ? 'bg-green-100 text-green-700' : (f.status === 'completed_with_errors' ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700')}">${f.status}</span></td>
<td class="px-3 py-2 text-sm">${f.imported_count}</td>
<td class="px-3 py-2 text-sm">${f.errors}</td>
<td class="px-3 py-2 text-sm">${hvCell}</td>
<td class="px-3 py-2 text-sm">${f.message || ''}</td>
</tr>
`;
}).join('');
const hasFailed = Number(data.audit.failed_files || 0) > 0;
const content = `
<div class="space-y-2 text-sm">
@@ -1396,6 +1643,7 @@ async function viewAuditDetails(auditId) {
<th class="px-3 py-2 text-left">Status</th>
<th class="px-3 py-2 text-left">Imported</th>
<th class="px-3 py-2 text-left">Errors</th>
<th class="px-3 py-2 text-left">Header Issues</th>
<th class="px-3 py-2 text-left">Message</th>
</tr>
</thead>