transfer auto-detect header

This commit is contained in:
r.mubarakzyanov 2026-03-31 13:41:31 +03:00
parent 4f0dbc7e91
commit d983659000
3 changed files with 151 additions and 61 deletions

View File

@ -198,7 +198,13 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
<div class="card-body">
<div class="mb-4">
<label class="form-label">Upload XLS Example:</label>
<input type="file" id="xlsUpload" class="form-control">
<div class="d-flex align-items-center gap-3">
<input type="file" id="xlsUpload" class="form-control" style="flex:1;">
<label class="form-check-label" style="white-space:nowrap; cursor:pointer;">
<input type="checkbox" class="form-check-input" id="autoDetectHeader" checked>
Auto-detect header row
</label>
</div>
<small id="uploadStatus">
<?php if (!empty($template['sample_xlsx'])): ?>
Current file: <a href="xlstemplates/<?php echo htmlspecialchars($template['sample_xlsx']); ?>" target="_blank"><?php echo htmlspecialchars($template['sample_xlsx']); ?></a>
@ -508,8 +514,6 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
type: 'array'
});
let sheet = workbook.Sheets[workbook.SheetNames[0]];
let rowIndex = parseInt(document.getElementById('headerRow').textContent) || 1;
let startColumn = parseInt(document.getElementById('startColumn').textContent) || 1;
let sheetData = XLSX.utils.sheet_to_json(sheet, {
header: 1,
@ -517,37 +521,152 @@ $xlsHeaders = $template['xls_headers'] ? json_decode($template['xls_headers'], t
raw: false,
range: 0
});
console.log("Dati della riga " + rowIndex + ":", sheetData[rowIndex - 1]);
if (!sheetData[rowIndex - 1]) {
const useAutoDetect = document.getElementById('autoDetectHeader').checked;
if (!useAutoDetect) {
// Use values from template settings
let rowIndex = parseInt(document.getElementById('headerRow').textContent) || 1;
let startColumn = parseInt(document.getElementById('startColumn').textContent) || 1;
console.log(`Manual mode: row ${rowIndex}, startCol ${startColumn}`);
if (!sheetData[rowIndex - 1]) {
document.getElementById('schemaFieldsBody').querySelectorAll('select.xls-columns').forEach(select => {
select.innerHTML = '<option value="">Nessuna intestazione trovata</option>';
});
return;
}
let headers = sheetData[rowIndex - 1].slice(startColumn - 1).map(header => header === undefined ? "" : String(header).trim());
console.log("Intestazioni estratte (manual):", headers);
availableXlsColumns = [...headers];
usedColumnsFromDB = [];
saveXlsHeaders(headers, rowIndex, startColumn);
updateXlsDropdowns();
return;
}
// Collect field_label titles from the schema table
const knownLabels = [];
document.querySelectorAll('.title-col').forEach(td => {
const v = (td.textContent || '').trim().toLowerCase().replace(/\s*required\s*$/i, '').trim();
if (v && v !== 'n/a') knownLabels.push(v);
});
const uniqueLabels = [...new Set(knownLabels)];
console.group('🔍 Auto-detect header row');
console.log('Sheet name:', workbook.SheetNames[0]);
console.log('Total rows in sheet:', sheetData.length);
console.log('Labels from schema field titles:', knownLabels);
console.log('Unique labels to match against:', uniqueLabels);
// Auto-detect: find the row with most matches, fallback to most non-empty unique text cells
let bestRow = 0;
let bestScore = 0;
let bestStartCol = 0;
let fallbackRow = 0;
let fallbackScore = 0;
let fallbackStartCol = 0;
const scanLimit = Math.min(sheetData.length, 50);
const rowScores = [];
for (let r = 0; r < scanLimit; r++) {
const row = sheetData[r] || [];
let score = 0;
let firstNonEmpty = -1;
let nonEmptyCount = 0;
const matched = [];
const cellValues = [];
const seen = new Set();
for (let c = 0; c < row.length; c++) {
const cellVal = String(row[c] || '').trim();
const cellLower = cellVal.toLowerCase();
if (cellVal) {
cellValues.push(`[${c}]="${cellVal}"`);
if (firstNonEmpty < 0) firstNonEmpty = c;
// Count unique short text cells (likely headers, not data/descriptions)
if (cellVal.length < 80 && !seen.has(cellLower)) {
nonEmptyCount++;
seen.add(cellLower);
}
}
if (cellLower && uniqueLabels.includes(cellLower)) {
score++;
matched.push(cellVal);
}
}
rowScores.push({ row: r + 1, score, nonEmpty: nonEmptyCount, matched, firstNonEmpty: firstNonEmpty + 1, cells: cellValues });
if (score > bestScore) {
bestScore = score;
bestRow = r;
bestStartCol = firstNonEmpty >= 0 ? firstNonEmpty : 0;
}
// Fallback: row with most unique short text cells (likely header row)
if (nonEmptyCount > fallbackScore) {
fallbackScore = nonEmptyCount;
fallbackRow = r;
fallbackStartCol = firstNonEmpty >= 0 ? firstNonEmpty : 0;
}
}
// If no label matches, use fallback (most populated row)
if (bestScore === 0 && fallbackScore > 0) {
bestRow = fallbackRow;
bestStartCol = fallbackStartCol;
console.log(`⚠️ No label matches found. Using fallback: row with most headers (${fallbackScore} unique cells)`);
}
console.log('All rows scanned:');
console.table(rowScores.filter(r => r.nonEmpty > 0).map(r => ({
row: r.row,
labelMatches: r.score,
uniqueCells: r.nonEmpty,
matched: r.matched.join(', '),
firstCol: r.firstNonEmpty
})));
console.log(`✅ Result: row ${bestRow + 1}, startCol ${bestStartCol + 1}, labelScore ${bestScore}/${uniqueLabels.length}, fallbackScore ${fallbackScore}`);
console.log('Selected row raw data:', sheetData[bestRow]);
console.groupEnd();
// Update display
document.getElementById('headerRow').textContent = bestRow + 1;
document.getElementById('startColumn').textContent = bestStartCol + 1;
if (!sheetData[bestRow]) {
document.getElementById('schemaFieldsBody').querySelectorAll('select.xls-columns').forEach(select => {
select.innerHTML = '<option value="">Nessuna intestazione trovata</option>';
});
return;
}
let headers = sheetData[rowIndex - 1].slice(startColumn - 1).map(header => header === undefined ? "" : header);
let headers = sheetData[bestRow].slice(bestStartCol).map(header => header === undefined ? "" : String(header).trim());
console.log("Intestazioni estratte:", headers);
availableXlsColumns = [...headers];
usedColumnsFromDB = [];
saveXlsHeaders(headers);
saveXlsHeaders(headers, bestRow + 1, bestStartCol + 1);
updateXlsDropdowns();
};
reader.readAsArrayBuffer(file);
}
function saveXlsHeaders(headers) {
function saveXlsHeaders(headers, headerRow, startColumn) {
const payload = {
template_id: <?php echo $id; ?>,
xls_headers: JSON.stringify(headers)
};
if (headerRow !== undefined) payload.header_row = headerRow;
if (startColumn !== undefined) payload.start_column = startColumn;
fetch('update_xls_headers.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
template_id: <?php echo $id; ?>,
xls_headers: JSON.stringify(headers)
})
body: JSON.stringify(payload)
}).then(response => response.json())
.then(data => {
if (!data.success) console.error("❌ Error saving XLS headers:", data.message);
else console.log("✅ Saved headers, header_row:", headerRow, "start_column:", startColumn);
})
.catch(error => console.error("❌ Fetch error:", error));
}

View File

@ -58,11 +58,6 @@ try {
$stmt->execute([$template_id]);
$mappings = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Also fetch excel_column labels for auto-detect
$stmtLabels = $pdo->prepare("SELECT excel_column FROM template_mapping WHERE template_id = ? AND is_manual = 0 AND excel_column IS NOT NULL AND excel_column != ''");
$stmtLabels->execute([$template_id]);
$templateLabels = array_map('trim', array_map('mb_strtolower', $stmtLabels->fetchAll(PDO::FETCH_COLUMN)));
// Debug dei mapping
error_log("Mappings found for template_id $template_id: " . print_r($mappings, true));
@ -76,47 +71,6 @@ try {
$highestColumn = $worksheet->getHighestColumn();
$highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
// ── Auto-detect header row and start column ──
// Scan all rows/columns to find the best match against template excel_column labels
if (!empty($templateLabels)) {
$bestRow = max(1, $header_row);
$bestCol = max(1, $start_column);
$bestScore = 0;
$scanLimit = min($highestRow, 50);
for ($scanRow = 1; $scanRow <= $scanLimit; $scanRow++) {
$score = 0;
for ($col = 1; $col <= $highestColumnIndex; $col++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
$cellVal = $worksheet->getCell($colLetter . $scanRow)->getCalculatedValue();
$cellVal = mb_strtolower(trim((string)$cellVal));
if ($cellVal !== '' && in_array($cellVal, $templateLabels, true)) {
$score++;
}
}
if ($score > $bestScore) {
$bestScore = $score;
$bestRow = $scanRow;
}
}
$header_row = $bestRow;
// Determine start_column: first non-empty cell in the detected header row
for ($col = 1; $col <= $highestColumnIndex; $col++) {
$colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col);
$cellVal = $worksheet->getCell($colLetter . $header_row)->getCalculatedValue();
if (trim((string)$cellVal) !== '') {
$start_column = $col;
break;
}
}
error_log("Auto-detected header row: $header_row, start column: $start_column (score: $bestScore/" . count($templateLabels) . ")");
$response['auto_header_row'] = $header_row;
$response['auto_header_score'] = $bestScore;
$response['auto_header_total'] = count($templateLabels);
}
$startRow = max(1, $header_row);
$startColumn = max(1, $start_column);

View File

@ -18,10 +18,27 @@ if (!$data || !isset($data['template_id'], $data['xls_headers'])) {
$templateId = $data['template_id'];
$xlsHeaders = $data['xls_headers'];
$headerRow = isset($data['header_row']) ? (int)$data['header_row'] : null;
$startColumn = isset($data['start_column']) ? (int)$data['start_column'] : null;
try {
$stmt = $pdo->prepare("UPDATE excel_templates SET xls_headers = ? WHERE id = ?");
$result = $stmt->execute([$xlsHeaders, $templateId]);
$sql = "UPDATE excel_templates SET xls_headers = ?";
$params = [$xlsHeaders];
if ($headerRow !== null) {
$sql .= ", header_row = ?";
$params[] = $headerRow;
}
if ($startColumn !== null) {
$sql .= ", start_column = ?";
$params[] = $startColumn;
}
$sql .= " WHERE id = ?";
$params[] = $templateId;
$stmt = $pdo->prepare($sql);
$result = $stmt->execute($params);
if (!$result) {
echo json_encode(["success" => false, "message" => "Database update failed"]);