699 lines
25 KiB
JavaScript
699 lines
25 KiB
JavaScript
// Configuration
|
|
const CONFIG = {
|
|
REQUIRED_EXCEL_COLUMNS: ['field', 'sub_field', 'year', 'season_start', 'season_end', 'tonnage_ha'],
|
|
OPTIONAL_EXCEL_COLUMNS: ['age', 'sub_area'], // age is calculated in script, sub_area is optional
|
|
REQUIRED_GEOJSON_PROPERTIES: ['field', 'sub_field'],
|
|
VALID_CRS: 'EPSG:32736', // UTM 36S - the correct CRS we learned from the conversation
|
|
CRS_DESCRIPTION: 'EPSG:32736 (UTM Zone 36S) - This is the correct CRS learned from geospatial analysis of Angata farm coordinates'
|
|
};
|
|
|
|
let excelData = null;
|
|
let geojsonData = null;
|
|
let excelLoaded = false;
|
|
let geojsonLoaded = false;
|
|
|
|
// File input handlers
|
|
document.getElementById('excelFile').addEventListener('change', handleExcelFile);
|
|
document.getElementById('geojsonFile').addEventListener('change', handleGeojsonFile);
|
|
document.getElementById('checkButton').addEventListener('click', validateData);
|
|
|
|
function updateCheckButton() {
|
|
const checkButton = document.getElementById('checkButton');
|
|
if (excelLoaded && geojsonLoaded) {
|
|
checkButton.style.display = 'inline-block';
|
|
} else {
|
|
checkButton.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Drag and drop handlers for Excel
|
|
const excelDropZone = document.getElementById('excelDropZone');
|
|
excelDropZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
excelDropZone.style.backgroundColor = '#f0f1ff';
|
|
});
|
|
excelDropZone.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
excelDropZone.style.backgroundColor = 'transparent';
|
|
});
|
|
excelDropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
excelDropZone.style.backgroundColor = 'transparent';
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
document.getElementById('excelFile').files = files;
|
|
handleExcelFile({ target: { files: files } });
|
|
}
|
|
});
|
|
|
|
// Drag and drop handlers for GeoJSON
|
|
const geojsonDropZone = document.getElementById('geojsonDropZone');
|
|
geojsonDropZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
geojsonDropZone.style.backgroundColor = '#f0f1ff';
|
|
});
|
|
geojsonDropZone.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
geojsonDropZone.style.backgroundColor = 'transparent';
|
|
});
|
|
geojsonDropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
geojsonDropZone.style.backgroundColor = 'transparent';
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
document.getElementById('geojsonFile').files = files;
|
|
handleGeojsonFile({ target: { files: files } });
|
|
}
|
|
});
|
|
|
|
function handleExcelFile(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
document.getElementById('excelFileName').textContent = `✓ ${file.name}`;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
try {
|
|
const data = new Uint8Array(event.target.result);
|
|
const workbook = XLSX.read(data, { type: 'array' });
|
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
excelData = XLSX.utils.sheet_to_json(worksheet);
|
|
excelLoaded = true;
|
|
updateCheckButton();
|
|
} catch (error) {
|
|
document.getElementById('excelFileName').textContent = `✗ Error: ${error.message}`;
|
|
excelLoaded = false;
|
|
updateCheckButton();
|
|
}
|
|
};
|
|
reader.onerror = () => {
|
|
document.getElementById('excelFileName').textContent = `✗ Failed to read file`;
|
|
excelLoaded = false;
|
|
updateCheckButton();
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
}
|
|
|
|
function handleGeojsonFile(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
document.getElementById('geojsonFileName').textContent = `✓ ${file.name}`;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
try {
|
|
geojsonData = JSON.parse(event.target.result);
|
|
geojsonLoaded = true;
|
|
updateCheckButton();
|
|
} catch (error) {
|
|
document.getElementById('geojsonFileName').textContent = `✗ Invalid JSON: ${error.message}`;
|
|
geojsonLoaded = false;
|
|
updateCheckButton();
|
|
}
|
|
};
|
|
reader.onerror = () => {
|
|
document.getElementById('geojsonFileName').textContent = `✗ Failed to read file`;
|
|
geojsonLoaded = false;
|
|
updateCheckButton();
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
function validateData() {
|
|
if (!excelData || !geojsonData) {
|
|
alert('Please upload both Excel and GeoJSON files before checking.');
|
|
return;
|
|
}
|
|
|
|
const results = {
|
|
checks: [],
|
|
details: []
|
|
};
|
|
|
|
// 1. Excel column validation
|
|
const excelColumnCheck = validateExcelColumns();
|
|
results.checks.push(excelColumnCheck);
|
|
results.details.push(excelColumnCheck.details);
|
|
|
|
// 2. GeoJSON properties validation
|
|
const geojsonPropsCheck = validateGeojsonProperties();
|
|
results.checks.push(geojsonPropsCheck);
|
|
results.details.push(geojsonPropsCheck.details);
|
|
|
|
// 3. CRS validation
|
|
const crsCheck = validateCRS();
|
|
results.checks.push(crsCheck);
|
|
results.details.push(crsCheck.details);
|
|
|
|
// 4. Field name matching
|
|
const fieldMatchCheck = validateFieldMatching();
|
|
results.checks.push(fieldMatchCheck);
|
|
results.details.push(fieldMatchCheck.details);
|
|
|
|
// 5. Data type and content validation
|
|
const dataValidationCheck = validateDataTypes();
|
|
results.checks.push(dataValidationCheck);
|
|
results.details.push(dataValidationCheck.details);
|
|
|
|
displayResults(results);
|
|
}
|
|
|
|
function validateExcelColumns() {
|
|
const excelColumns = Object.keys(excelData[0] || {});
|
|
const missing = CONFIG.REQUIRED_EXCEL_COLUMNS.filter(col => !excelColumns.includes(col));
|
|
const hasOptional = CONFIG.OPTIONAL_EXCEL_COLUMNS.filter(col => excelColumns.includes(col));
|
|
const notRequired = excelColumns.filter(col => !CONFIG.REQUIRED_EXCEL_COLUMNS.includes(col) && !CONFIG.OPTIONAL_EXCEL_COLUMNS.includes(col));
|
|
|
|
let status = 'pass';
|
|
let message = 'All required columns present';
|
|
|
|
if (missing.length > 0) {
|
|
status = 'fail';
|
|
message = `Missing required columns: ${missing.join(', ')}`;
|
|
} else if (notRequired.length > 0) {
|
|
status = 'warning';
|
|
message = `Extra columns detected (will be ignored): ${notRequired.join(', ')}`;
|
|
}
|
|
|
|
return {
|
|
name: 'Excel Columns',
|
|
status: status,
|
|
message: message,
|
|
details: {
|
|
title: 'Excel Column Validation',
|
|
type: 'columns',
|
|
required: CONFIG.REQUIRED_EXCEL_COLUMNS,
|
|
optional: CONFIG.OPTIONAL_EXCEL_COLUMNS,
|
|
found: excelColumns,
|
|
missing: missing,
|
|
hasOptional: hasOptional,
|
|
extra: notRequired
|
|
}
|
|
};
|
|
}
|
|
|
|
function validateGeojsonProperties() {
|
|
if (!geojsonData.features || geojsonData.features.length === 0) {
|
|
return {
|
|
name: 'GeoJSON Properties',
|
|
status: 'fail',
|
|
message: 'GeoJSON has no features',
|
|
details: {
|
|
title: 'GeoJSON Property Validation',
|
|
type: 'properties',
|
|
error: 'No features found in GeoJSON'
|
|
}
|
|
};
|
|
}
|
|
|
|
const allProperties = new Set();
|
|
const missingInFeatures = [];
|
|
|
|
geojsonData.features.forEach((feature, idx) => {
|
|
const props = feature.properties || {};
|
|
Object.keys(props).forEach(p => allProperties.add(p));
|
|
|
|
CONFIG.REQUIRED_GEOJSON_PROPERTIES.forEach(reqProp => {
|
|
if (!props[reqProp]) {
|
|
missingInFeatures.push({ feature: idx, property: reqProp, field: props.field || 'Unknown' });
|
|
}
|
|
});
|
|
});
|
|
|
|
const extra = Array.from(allProperties).filter(p => !CONFIG.REQUIRED_GEOJSON_PROPERTIES.includes(p));
|
|
|
|
let status = 'pass';
|
|
let message = 'All required properties present in all features';
|
|
|
|
if (missingInFeatures.length > 0) {
|
|
status = 'fail';
|
|
message = `Missing properties in ${missingInFeatures.length} feature(s)`;
|
|
} else if (extra.length > 0) {
|
|
status = 'warning';
|
|
message = `Extra properties detected: ${extra.join(', ')}`;
|
|
}
|
|
|
|
return {
|
|
name: 'GeoJSON Properties',
|
|
status: status,
|
|
message: message,
|
|
details: {
|
|
title: 'GeoJSON Property Validation',
|
|
type: 'properties',
|
|
required: CONFIG.REQUIRED_GEOJSON_PROPERTIES,
|
|
found: Array.from(allProperties),
|
|
extra: extra,
|
|
missingInFeatures: missingInFeatures
|
|
}
|
|
};
|
|
}
|
|
|
|
function validateCRS() {
|
|
const crs = geojsonData.crs;
|
|
let detectedCRS = 'Not specified';
|
|
let status = 'fail';
|
|
let message = `CRS not specified. Expected: ${CONFIG.VALID_CRS}`;
|
|
|
|
if (crs) {
|
|
if (crs.type === 'name' && crs.properties?.name) {
|
|
detectedCRS = crs.properties.name;
|
|
// Check for various CRS string formats
|
|
if (detectedCRS.includes('32736') || detectedCRS.includes('UTM') && detectedCRS.includes('36')) {
|
|
status = 'pass';
|
|
message = `✓ Correct CRS detected: ${detectedCRS}`;
|
|
} else {
|
|
status = 'fail';
|
|
message = `Wrong CRS: ${detectedCRS}. Expected: ${CONFIG.VALID_CRS}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: 'Coordinate Reference System',
|
|
status: status,
|
|
message: message,
|
|
details: {
|
|
title: 'CRS Validation',
|
|
type: 'crs',
|
|
expected: CONFIG.VALID_CRS,
|
|
description: CONFIG.CRS_DESCRIPTION,
|
|
detected: detectedCRS,
|
|
crsObject: crs
|
|
}
|
|
};
|
|
}
|
|
|
|
function validateFieldMatching() {
|
|
const excelFields = new Set(excelData.map(row => String(row.field).trim()));
|
|
const geojsonFields = new Set(geojsonData.features.map(f => String(f.properties.field).trim()));
|
|
|
|
const matchingFields = Array.from(excelFields).filter(f => geojsonFields.has(f));
|
|
const excelOnly = Array.from(excelFields).filter(f => !geojsonFields.has(f));
|
|
const geojsonOnly = Array.from(geojsonFields).filter(f => !excelFields.has(f));
|
|
|
|
let status = 'pass';
|
|
let message = 'All field names match between Excel and GeoJSON';
|
|
|
|
if (excelOnly.length > 0 || geojsonOnly.length > 0) {
|
|
status = 'fail';
|
|
message = `Field name mismatches detected: ${excelOnly.length} in Excel only, ${geojsonOnly.length} in GeoJSON only`;
|
|
}
|
|
|
|
// Create matching table
|
|
const matchingTable = [];
|
|
excelFields.forEach(field => {
|
|
const inGeojson = geojsonFields.has(field);
|
|
matchingTable.push({
|
|
field: field,
|
|
excel: true,
|
|
geojson: inGeojson,
|
|
status: inGeojson ? 'match' : 'mismatch'
|
|
});
|
|
});
|
|
|
|
geojsonOnly.forEach(field => {
|
|
matchingTable.push({
|
|
field: field,
|
|
excel: false,
|
|
geojson: true,
|
|
status: 'mismatch'
|
|
});
|
|
});
|
|
|
|
return {
|
|
name: 'Field Name Matching',
|
|
status: status,
|
|
message: message,
|
|
details: {
|
|
title: 'Field Name Matching',
|
|
type: 'fieldMatching',
|
|
matching: matchingFields,
|
|
excelOnly: excelOnly,
|
|
geojsonOnly: geojsonOnly,
|
|
matchingTable: matchingTable
|
|
}
|
|
};
|
|
}
|
|
|
|
function validateDataTypes() {
|
|
const issues = [];
|
|
const missingDates = [];
|
|
const invalidYears = [];
|
|
const invalidNumerics = [];
|
|
|
|
excelData.forEach((row, idx) => {
|
|
// Check season_start
|
|
if (!row.season_start || row.season_start === '') {
|
|
missingDates.push({ row: idx + 2, field: row.field, column: 'season_start' });
|
|
} else if (!isValidDate(row.season_start)) {
|
|
invalidYears.push({ row: idx + 2, field: row.field, column: 'season_start', value: row.season_start });
|
|
}
|
|
|
|
// Check year
|
|
if (!Number.isInteger(parseFloat(row.year))) {
|
|
invalidYears.push({ row: idx + 2, field: row.field, column: 'year', value: row.year });
|
|
}
|
|
|
|
// Check numeric columns (age is optional, sub_area is text, not numeric)
|
|
['tonnage_ha'].forEach(col => {
|
|
const val = row[col];
|
|
if (val !== '' && val !== null && isNaN(parseFloat(val))) {
|
|
invalidNumerics.push({ row: idx + 2, field: row.field, column: col, value: val });
|
|
}
|
|
});
|
|
});
|
|
|
|
let status = 'pass';
|
|
let message = 'All data types valid';
|
|
|
|
if (missingDates.length > 0 || invalidYears.length > 0 || invalidNumerics.length > 0) {
|
|
status = 'warning';
|
|
message = `Data validation issues found: ${missingDates.length} missing dates, ${invalidYears.length} invalid years/dates, ${invalidNumerics.length} invalid numerics`;
|
|
}
|
|
|
|
return {
|
|
name: 'Data Validation',
|
|
status: status,
|
|
message: message,
|
|
details: {
|
|
title: 'Data Type & Content Validation',
|
|
type: 'dataValidation',
|
|
missingDates: missingDates,
|
|
invalidYears: invalidYears,
|
|
invalidNumerics: invalidNumerics
|
|
}
|
|
};
|
|
}
|
|
|
|
function isValidDate(dateString) {
|
|
if (!dateString) return false;
|
|
const date = new Date(dateString);
|
|
return date instanceof Date && !isNaN(date);
|
|
}
|
|
|
|
function displayResults(results) {
|
|
const trafficLight = document.getElementById('trafficLight');
|
|
const detailsSection = document.getElementById('detailsSection');
|
|
const resultsSection = document.getElementById('resultsSection');
|
|
|
|
trafficLight.innerHTML = '';
|
|
detailsSection.innerHTML = '';
|
|
|
|
// Display traffic lights
|
|
results.checks.forEach(check => {
|
|
const light = document.createElement('div');
|
|
light.className = `check-item ${check.status}`;
|
|
light.innerHTML = `
|
|
<span class="light ${check.status === 'pass' ? 'green' : check.status === 'warning' ? 'yellow' : 'red'}"></span>
|
|
<div>
|
|
<strong>${check.name}</strong>
|
|
<div style="font-size: 13px; margin-top: 4px;">${check.message}</div>
|
|
</div>
|
|
`;
|
|
trafficLight.appendChild(light);
|
|
});
|
|
|
|
// Display details
|
|
results.details.forEach(detail => {
|
|
if (detail.type === 'columns') {
|
|
detailsSection.appendChild(createColumnDetails(detail));
|
|
} else if (detail.type === 'properties') {
|
|
detailsSection.appendChild(createPropertiesDetails(detail));
|
|
} else if (detail.type === 'crs') {
|
|
detailsSection.appendChild(createCRSDetails(detail));
|
|
} else if (detail.type === 'fieldMatching') {
|
|
detailsSection.appendChild(createFieldMatchingDetails(detail));
|
|
} else if (detail.type === 'dataValidation') {
|
|
detailsSection.appendChild(createDataValidationDetails(detail));
|
|
}
|
|
});
|
|
|
|
resultsSection.classList.add('show');
|
|
}
|
|
|
|
function createColumnDetails(detail) {
|
|
const section = document.createElement('div');
|
|
section.innerHTML = `<h3>${detail.title}</h3>`;
|
|
|
|
// Required columns
|
|
section.innerHTML += `
|
|
<div style="margin-bottom: 15px;">
|
|
<strong>Required Columns:</strong>
|
|
<div class="field-list" style="margin-top: 8px;">
|
|
${detail.required.map(col => `<div class="field-badge" style="border-left-color: #28a745; background: #d4edda; color: #155724;">${col}</div>`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Optional columns
|
|
if (detail.optional && detail.optional.length > 0) {
|
|
section.innerHTML += `
|
|
<div style="margin-bottom: 15px;">
|
|
<strong>Optional Columns (not required):</strong>
|
|
<div class="field-list" style="margin-top: 8px;">
|
|
${detail.optional.map(col => `<div class="field-badge" style="border-left-color: #17a2b8; background: #d1ecf1; color: #0c5460;">${col}</div>`).join('')}
|
|
</div>
|
|
<small style="display: block; margin-top: 8px;">✓ <em>${detail.optional.join(', ')} ${detail.optional.length === 1 ? 'is' : 'are'} calculated in the system or optional</em></small>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.missing.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box error">
|
|
<strong>❌ Missing Required Columns:</strong><br>${detail.missing.join(', ')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.extra.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box warning">
|
|
<strong>⚠️ Extra Columns (will be ignored):</strong><br>${detail.extra.join(', ')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.missing.length === 0 && detail.extra.length === 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box success">
|
|
<strong>✓ Perfect!</strong> All required columns present.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return section;
|
|
}
|
|
|
|
function createPropertiesDetails(detail) {
|
|
const section = document.createElement('div');
|
|
section.innerHTML = `<h3>${detail.title}</h3>`;
|
|
|
|
if (detail.error) {
|
|
section.innerHTML += `<div class="message-box error">${detail.error}</div>`;
|
|
return section;
|
|
}
|
|
|
|
if (detail.missingInFeatures && detail.missingInFeatures.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box error">
|
|
<strong>❌ Missing Properties in Features:</strong>
|
|
<table>
|
|
<tr><th>Feature #</th><th>Field Name</th><th>Missing Property</th></tr>
|
|
${detail.missingInFeatures.map(m => `<tr><td>${m.feature}</td><td>${m.field}</td><td>${m.property}</td></tr>`).join('')}
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.extra && detail.extra.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box warning">
|
|
<strong>⚠️ Extra Properties (redundant):</strong><br>${detail.extra.join(', ')}<br>
|
|
<small>These will be ignored during processing.</small>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if ((!detail.missingInFeatures || detail.missingInFeatures.length === 0) && (!detail.extra || detail.extra.length === 0)) {
|
|
section.innerHTML += `
|
|
<div class="message-box success">
|
|
<strong>✓ Perfect!</strong> All required properties present in all ${geojsonData.features.length} features.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return section;
|
|
}
|
|
|
|
function createCRSDetails(detail) {
|
|
const section = document.createElement('div');
|
|
section.innerHTML = `<h3>${detail.title}</h3>`;
|
|
|
|
if (detail.detected === 'Not specified') {
|
|
section.innerHTML += `
|
|
<div class="message-box error">
|
|
<strong>❌ CRS Not Specified</strong><br>
|
|
Expected: <code>${detail.expected}</code><br>
|
|
${detail.description}
|
|
</div>
|
|
`;
|
|
} else if (detail.detected.includes('32736') || (detail.detected.includes('UTM') && detail.detected.includes('36'))) {
|
|
section.innerHTML += `
|
|
<div class="message-box success">
|
|
<strong>✓ Correct CRS</strong><br>
|
|
Detected: <code>${detail.detected}</code><br>
|
|
${detail.description}
|
|
</div>
|
|
`;
|
|
} else {
|
|
section.innerHTML += `
|
|
<div class="message-box error">
|
|
<strong>❌ Wrong CRS</strong><br>
|
|
Expected: <code>${detail.expected}</code><br>
|
|
Detected: <code>${detail.detected}</code><br>
|
|
${detail.description}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.crsObject) {
|
|
section.innerHTML += `
|
|
<div style="margin-top: 15px; padding: 10px; background: #f8f9ff; border-radius: 4px; font-size: 12px;">
|
|
<strong>CRS Details:</strong><br>
|
|
<code>${JSON.stringify(detail.crsObject, null, 2)}</code>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return section;
|
|
}
|
|
|
|
function createFieldMatchingDetails(detail) {
|
|
const section = document.createElement('div');
|
|
section.innerHTML = `<h3>${detail.title}</h3>`;
|
|
|
|
if (detail.excelOnly.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box error">
|
|
<strong>❌ Fields in Excel but NOT in GeoJSON (${detail.excelOnly.length}):</strong>
|
|
<div class="field-list">
|
|
${detail.excelOnly.map(f => `<div class="field-badge missing">${f}</div>`).join('')}
|
|
</div>
|
|
<small style="display: block; margin-top: 10px;">These fields exist in your harvest data but have no boundaries defined in the GeoJSON.</small>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.geojsonOnly.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box error">
|
|
<strong>❌ Fields in GeoJSON but NOT in Excel (${detail.geojsonOnly.length}):</strong>
|
|
<div class="field-list">
|
|
${detail.geojsonOnly.map(f => `<div class="field-badge extra">${f}</div>`).join('')}
|
|
</div>
|
|
<small style="display: block; margin-top: 10px;">These fields have boundaries defined but no data in your harvest spreadsheet.</small>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.matching.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box success">
|
|
<strong>✓ Matching Fields (${detail.matching.length}):</strong>
|
|
<div class="field-list">
|
|
${detail.matching.map(f => `<div class="field-badge" style="border-left-color: #28a745; background: #d4edda; color: #155724;">${f}</div>`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Full matching table
|
|
section.innerHTML += `
|
|
<div style="margin-top: 20px;">
|
|
<strong>Complete Field Summary:</strong>
|
|
<table>
|
|
<tr>
|
|
<th>Field Name</th>
|
|
<th>In Excel</th>
|
|
<th>In GeoJSON</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
${detail.matchingTable.map(row => `
|
|
<tr>
|
|
<td><strong>${row.field}</strong></td>
|
|
<td>${row.excel ? '✓' : '✗'}</td>
|
|
<td>${row.geojson ? '✓' : '✗'}</td>
|
|
<td><span class="${row.status}">${row.status === 'match' ? '🟢 Match' : '🔴 Mismatch'}</span></td>
|
|
</tr>
|
|
`).join('')}
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
return section;
|
|
}
|
|
|
|
function createDataValidationDetails(detail) {
|
|
const section = document.createElement('div');
|
|
section.innerHTML = `<h3>${detail.title}</h3>`;
|
|
|
|
if (detail.missingDates.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box warning">
|
|
<strong>⚠️ Missing season_start dates (${detail.missingDates.length}):</strong>
|
|
<table style="font-size: 13px;">
|
|
<tr><th>Row #</th><th>Field Name</th></tr>
|
|
${detail.missingDates.map(m => `<tr><td>${m.row}</td><td>${m.field}</td></tr>`).join('')}
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.invalidYears.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box warning">
|
|
<strong>⚠️ Invalid dates/years (${detail.invalidYears.length}):</strong>
|
|
<table style="font-size: 13px;">
|
|
<tr><th>Row #</th><th>Field Name</th><th>Column</th><th>Value</th></tr>
|
|
${detail.invalidYears.map(m => `<tr><td>${m.row}</td><td>${m.field}</td><td>${m.column}</td><td>${m.value}</td></tr>`).join('')}
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.invalidNumerics.length > 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box warning">
|
|
<strong>⚠️ Invalid numeric values (${detail.invalidNumerics.length}):</strong>
|
|
<table style="font-size: 13px;">
|
|
<tr><th>Row #</th><th>Field Name</th><th>Column</th><th>Value</th></tr>
|
|
${detail.invalidNumerics.map(m => `<tr><td>${m.row}</td><td>${m.field}</td><td>${m.column}</td><td>${m.value}</td></tr>`).join('')}
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (detail.missingDates.length === 0 && detail.invalidYears.length === 0 && detail.invalidNumerics.length === 0) {
|
|
section.innerHTML += `
|
|
<div class="message-box success">
|
|
<strong>✓ All data types valid!</strong> No missing dates or invalid values detected.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return section;
|
|
}
|
|
|
|
function showError(fileType, message) {
|
|
alert(`${fileType} Error: ${message}`);
|
|
}
|