feat(import_edit2): enhance grid UI with sticky columns and textarea conversion

- Fix grid-top alignment and datepicker positioning
- Extend column resizer to grid-top cells
- Add sticky columns for Actions and Sample Description
- Implement input-to-textarea conversion on focus
- Increase Select2 dropdown height
- Improve grid-container CSS for sticky support
This commit is contained in:
Martin Grigoryan 2026-02-04 11:59:59 +04:00
parent f60dc64b2d
commit d8eddb3aa5

View File

@ -156,7 +156,8 @@ function fixedDefaultValue(array $f): string
}
input,
select {
select,
textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid #ced4da;
@ -166,10 +167,36 @@ function fixedDefaultValue(array $f): string
}
input,
select {
select,
textarea {
color: #333;
}
textarea {
resize: vertical;
min-height: 60px;
border: 1px solid #ced4da !important;
}
textarea:focus,
textarea:active,
textarea:hover {
border: 1px solid #ced4da !important;
outline: none !important;
}
textarea.auto-input {
background-color: #d4edda;
}
textarea.manual-input {
background-color: #fff3cd;
}
textarea.required-input {
background-color: #f8d7da;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
@ -197,11 +224,12 @@ function fixedDefaultValue(array $f): string
.grid-container {
overflow-x: auto;
max-width: 100%;
width: 100%;
margin-bottom: 20px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.grid-row {
@ -249,6 +277,55 @@ function fixedDefaultValue(array $f): string
border-right: none;
}
/* Sticky columns - first column (Actions) */
.grid-header.button-header,
.grid-cell.button-cell {
position: sticky !important;
left: 0;
z-index: 9;
background: white;
overflow: visible;
flex-shrink: 0;
}
.grid-header.button-header {
background-color: #e9ecef;
}
.grid-row:nth-child(even) .grid-cell.button-cell {
background-color: #f8f9fa;
}
.grid-row:hover .grid-cell.button-cell {
background-color: #e9ecef;
}
.grid-row .grid-header:nth-child(2),
.grid-row .grid-cell:nth-child(2) {
position: sticky !important;
left: 210px;
z-index: 8;
background: white;
overflow: visible;
flex-shrink: 0;
}
.grid-row .grid-header:nth-child(2) {
background-color: #e9ecef;
}
.grid-row:nth-child(even) .grid-cell:nth-child(2) {
background-color: #f8f9fa;
}
.grid-row:hover .grid-cell:nth-child(2) {
background-color: #e9ecef;
}
.grid-row {
position: relative;
}
.grid-cell.expanded {
max-width: 500px !important;
white-space: normal !important;
@ -276,6 +353,8 @@ function fixedDefaultValue(array $f): string
display: flex;
align-items: flex-start;
padding: 10px 0;
min-height: 0;
flex-wrap: nowrap;
}
.grid-top .grid-cell {
@ -285,6 +364,7 @@ function fixedDefaultValue(array $f): string
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
}
.grid-top .save-all-cell {
@ -292,6 +372,15 @@ function fixedDefaultValue(array $f): string
align-items: flex-start;
}
.grid-top .grid-cell input,
.grid-top .grid-cell select,
.grid-top .grid-cell .date-picker,
.grid-top .grid-cell .fixed-top {
position: relative;
max-width: 100%;
z-index: 1;
}
.propagate-btn {
background: none;
border: none;
@ -489,6 +578,10 @@ function fixedDefaultValue(array $f): string
font-size: 14px;
}
.select2-results__options {
max-height: 400px !important;
}
.select2-container--default .select2-selection--single {
height: 31px;
border: 1px solid #ced4da;
@ -552,8 +645,12 @@ function fixedDefaultValue(array $f): string
<div class="grid-cell save-all-cell">
<button type="button" class="save-all-btn" title="Save All Rows"><i class="fas fa-save"></i> Save All</button>
</div>
<?php if ($mainFieldMapping): ?>
<div class="grid-cell" style="flex: 0 0 150px;">
<?php
$topColIndex = 1;
if ($mainFieldMapping): ?>
<div class="grid-cell grid-top-cell" style="flex: 0 0 150px;" data-index="1">
<?php
$fieldValue = $mainFieldMapping['manual_default'] ?? '';
if ($mainFieldMapping['data_type'] === 'Data' && $mainFieldMapping['manual_default'] === 'today') {
@ -578,35 +675,45 @@ function fixedDefaultValue(array $f): string
}
?>
</div>
<?php endif; ?>
<div class="grid-cell" style="flex: 0 0 300px;">
<?php endif;
$topColIndex = $mainFieldMapping ? 2 : 1; ?>
<div class="grid-cell grid-top-cell" style="flex: 0 0 300px;" data-index="<?= $mainFieldMapping ? 2 : 1 ?>">
<select class="custom-field dropdown-select client-select searchable-client" data-column="idclient" id="clientSelect" name="idclient">
<option value="">Select a client...</option>
</select>
<button type="button" class="propagate-btn" data-column="idclient"><i class="fas fa-arrow-down"></i></button>
<span id="clientLoadingStatus" class="text-muted" style="margin-left: 10px; display: none;">Recupero clienti in corso...</span>
</div>
<div class="grid-cell" style="flex: 0 0 150px;"></div>
<div class="grid-cell grid-top-cell" style="flex: 0 0 150px;" data-index="<?= $mainFieldMapping ? 3 : 2 ?>"></div>
<?php
$topColIndex = $mainFieldMapping ? 4 : 3;
$autoIndex = 0;
foreach ($allMappings as $mapping) {
if (!$mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
$inputClass = 'auto-input';
if ($mapping['is_required']) $inputClass .= ' required-input';
if ($mapping['data_type'] === 'SceltaMultipla') {
echo "<div class='grid-cell' style='flex: 0 0 150px;'>";
echo "<select class='custom-field dropdown-select $inputClass' data-column='auto_$autoIndex' data-field-id='{$mapping['field_id']}' data-selected-value='" . htmlspecialchars($fieldValue) . "' " . ($mapping['is_required'] ? 'required' : '') . ">";
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 150px;' data-index='$topColIndex'>";
echo "<select class='custom-field dropdown-select $inputClass' data-column='auto_$autoIndex' data-field-id='{$mapping['field_id']}' data-selected-value='" . htmlspecialchars($fieldValue ?? '') . "' " . ($mapping['is_required'] ? 'required' : '') . ">";
echo "<option value=''>Seleziona...</option>";
echo "</select>";
echo "<button type='button' class='propagate-btn' data-column='auto_$autoIndex'><i class='fas fa-arrow-down'></i></button>";
echo "</div>";
} else {
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 150px;' data-index='$topColIndex'></div>";
}
$autoIndex++;
$topColIndex++;
}
}
$manualIndex = 0;
foreach ($allMappings as $mapping) {
if ($mapping['is_manual'] && $mapping['main_field'] != 1 && $mapping['is_visible_import'] == 1) {
$fieldValue = $mapping['manual_default'] ?? '';
@ -615,7 +722,7 @@ function fixedDefaultValue(array $f): string
}
$inputClass = 'manual-input';
if ($mapping['is_required']) $inputClass .= ' required-input';
echo "<div class='grid-cell' style='flex: 0 0 150px;'>";
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 150px;' data-index='$topColIndex'>";
if ($mapping['data_type'] === 'SceltaMultipla') {
echo "<select class='custom-field dropdown-select $inputClass' data-column='manual_$manualIndex' data-field-id='{$mapping['field_id']}' data-selected-value='" . htmlspecialchars($fieldValue) . "' " . ($mapping['is_required'] ? 'required' : '') . ">";
echo "<option value=''>Seleziona...</option>";
@ -629,18 +736,21 @@ function fixedDefaultValue(array $f): string
}
echo "<button type='button' class='propagate-btn' data-column='manual_$manualIndex'><i class='fas fa-arrow-down'></i></button>";
echo "</div>";
$manualIndex++;
$topColIndex++;
}
}
// Aggiunta per Tested Component (senza propagate)
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
echo "<div class='grid-cell' style='flex: 0 0 200px;'></div>";
echo "<div class='grid-cell' style='flex: 0 0 250px;'></div>";
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
echo "<div class='grid-cell' style='flex: 0 0 150px;'></div>";
// ---------------- FIXED FIELDS TOP (propagate) ----------------
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 150px;' data-index='$topColIndex'></div>";
$topColIndex++;
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 200px;' data-index='$topColIndex'></div>";
$topColIndex++;
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 250px;' data-index='$topColIndex'></div>";
$topColIndex++;
// ---------------- FIXED FIELDS TOP (propagate) - same order as header: fixed cols, 3 empties after ConsegnaRichiesta ----------------
if (!empty($fixedFields)) {
$insertedAfterConsegnaTop = false;
foreach ($fixedFields as $fx => $f) {
$key = $f['fixed_field_key']; // datadb column
@ -651,7 +761,8 @@ function fixedDefaultValue(array $f): string
$inputClass = 'manual-input' . ($isRequired ? ' required-input' : '');
$topRequiredClass = ($isRequired && ($val === '' || $val === null)) ? 'missing-required' : '';
echo "<div class='grid-cell {$topRequiredClass}' style='flex: 0 0 180px;'>";
echo "<div class='grid-cell grid-top-cell {$topRequiredClass}' style='flex: 0 0 180px;' data-index='$topColIndex'>";
$topColIndex++;
// Forza DATE anche se per errore nel DB è diversa
$isDate = ($f['data_type'] === 'DATE' || $key === 'ConsegnaRichiesta');
@ -697,6 +808,16 @@ function fixedDefaultValue(array $f): string
// UNA SOLA freccia
echo "<button type='button' class='propagate-btn' data-column='fixed_{$fx}'><i class='fas fa-arrow-down'></i></button>";
echo "</div>";
if ($key === 'ConsegnaRichiesta' && !$insertedAfterConsegnaTop) {
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 150px;' data-index='$topColIndex'></div>";
$topColIndex++;
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 150px;' data-index='$topColIndex'></div>";
$topColIndex++;
echo "<div class='grid-cell grid-top-cell' style='flex: 0 0 150px;' data-index='$topColIndex'></div>";
$topColIndex++;
$insertedAfterConsegnaTop = true;
}
}
}
@ -880,7 +1001,7 @@ function fixedDefaultValue(array $f): string
echo "</div>";
$cellIndex++;
?>
<div class="grid-cell" style="flex: 0 0 200px;">
<div class="grid-cell" data-index="<?= $cellIndex ?>" style="flex: 0 0 200px;">
<select name="rows[<?= $index ?>][carrier]" class="carrier-select">
<option value="tnt-it">TNT Italy</option>
<option value="dhl">DHL</option>
@ -891,10 +1012,12 @@ function fixedDefaultValue(array $f): string
<input type="text" name="rows[<?= $index ?>][awb_number]" class="cell-input awb-input" placeholder="Inserisci AWB Number">
<button type="button" class="go-btn" data-row="<?= $index ?>"><i class="fas fa-play"></i></button>
</div>
<div class="grid-cell tracking-info" data-row="<?= $index ?>" style="flex: 0 0 250px;">
<?php $cellIndex++; ?>
<div class="grid-cell tracking-info" data-row="<?= $index ?>" data-index="<?= $cellIndex ?>" style="flex: 0 0 250px;">
<span class="tracking-result">Shipment Info</span>
<input type="hidden" name="rows[<?= $index ?>][tracking_info]" class="tracking-hidden">
</div>
<?php $cellIndex++; ?>
<?php
// ---------------- FIXED FIELDS CELLS ----------------
@ -1365,14 +1488,110 @@ function fixedDefaultValue(array $f): string
// Gestione degli input
inputs.forEach(input => {
input.addEventListener('focus', function() {
this.closest('.grid-cell').classList.add('expanded');
});
input.addEventListener('blur', function() {
this.closest('.grid-cell').classList.remove('expanded');
});
if (input.tagName === 'SELECT' ||
input.classList.contains('date-picker') ||
input.type === 'number' ||
input.type === 'date') {
input.addEventListener('focus', function() {
this.closest('.grid-cell').classList.add('expanded');
});
input.addEventListener('blur', function() {
this.closest('.grid-cell').classList.remove('expanded');
});
return;
}
if (input.type === 'text') {
input.addEventListener('focus', function() {
const cell = this.closest('.grid-cell');
cell.classList.add('expanded');
cell.dataset.convertingToTextarea = 'true';
const textarea = document.createElement('textarea');
textarea.value = this.value;
textarea.name = this.name;
textarea.className = this.className;
textarea.required = this.required;
textarea.style.border = '1px solid #ced4da';
textarea.setAttribute('data-mapping-id', this.getAttribute('data-mapping-id') || '');
textarea.setAttribute('data-field-id', this.getAttribute('data-field-id') || '');
textarea.setAttribute('data-selected-value', this.getAttribute('data-selected-value') || '');
textarea.setAttribute('data-current-value', this.getAttribute('data-current-value') || '');
textarea.setAttribute('data-fixed-key', this.getAttribute('data-fixed-key') || '');
textarea.dataset.originalInput = 'true';
this.parentNode.replaceChild(textarea, this);
setTimeout(() => {
textarea.focus();
delete cell.dataset.convertingToTextarea;
}, 0);
});
} else {
input.addEventListener('focus', function() {
this.closest('.grid-cell').classList.add('expanded');
});
}
});
document.addEventListener('blur', function(e) {
const element = e.target;
if (element.tagName === 'TEXTAREA' && element.dataset.originalInput === 'true') {
const cell = element.closest('.grid-cell');
const input = document.createElement('input');
input.type = 'text';
input.value = element.value;
input.name = element.name;
input.className = element.className;
input.required = element.required;
input.setAttribute('data-mapping-id', element.getAttribute('data-mapping-id') || '');
input.setAttribute('data-field-id', element.getAttribute('data-field-id') || '');
input.setAttribute('data-selected-value', element.getAttribute('data-selected-value') || '');
input.setAttribute('data-current-value', element.getAttribute('data-current-value') || '');
input.setAttribute('data-fixed-key', element.getAttribute('data-fixed-key') || '');
element.parentNode.replaceChild(input, element);
cell.classList.remove('expanded');
input.addEventListener('focus', function() {
const cell = this.closest('.grid-cell');
cell.classList.add('expanded');
cell.dataset.convertingToTextarea = 'true';
const textarea = document.createElement('textarea');
textarea.value = this.value;
textarea.name = this.name;
textarea.className = this.className;
textarea.required = this.required;
textarea.style.border = '1px solid #ced4da';
textarea.setAttribute('data-mapping-id', this.getAttribute('data-mapping-id') || '');
textarea.setAttribute('data-field-id', this.getAttribute('data-field-id') || '');
textarea.setAttribute('data-selected-value', this.getAttribute('data-selected-value') || '');
textarea.setAttribute('data-current-value', this.getAttribute('data-current-value') || '');
textarea.setAttribute('data-fixed-key', this.getAttribute('data-fixed-key') || '');
textarea.dataset.originalInput = 'true';
this.parentNode.replaceChild(textarea, this);
setTimeout(() => {
textarea.focus();
delete cell.dataset.convertingToTextarea;
}, 0);
});
const changeEvent = new Event('change', { bubbles: true });
input.dispatchEvent(changeEvent);
} else if (element.tagName === 'INPUT' && element.type === 'text') {
const cell = element.closest('.grid-cell');
if (cell && !cell.dataset.convertingToTextarea) {
cell.classList.remove('expanded');
}
}
}, true);
// Gestione della propagazione
const propagateButtons = document.querySelectorAll('.propagate-btn');
propagateButtons.forEach(button => {
@ -1453,12 +1672,14 @@ function fixedDefaultValue(array $f): string
function resize(e) {
if (currentResizer) {
const dx = e.pageX - startX;
const newWidth = startWidth + dx;
const deltaX = e.pageX - startX;
const newWidth = Math.max(80, startWidth + deltaX);
const headers = document.querySelectorAll(`.grid-header[data-index="${columnIndex}"]`);
const cells = document.querySelectorAll(`.grid-cell[data-index="${columnIndex}"]`);
const topCells = document.querySelectorAll(`.grid-top .grid-cell[data-index="${columnIndex}"]`);
headers.forEach(header => header.style.flex = `0 0 ${newWidth}px`);
cells.forEach(cell => cell.style.flex = `0 0 ${newWidth}px`);
topCells.forEach(cell => cell.style.flex = `0 0 ${newWidth}px`);
}
}
@ -2009,6 +2230,17 @@ function fixedDefaultValue(array $f): string
});
}
});
// Fix for sticky columns
const gridContainer = document.querySelector('.grid-container');
if (gridContainer) {
const rows = document.querySelectorAll('.grid-row');
rows.forEach(row => {
if (!row.style.minWidth) {
row.style.minWidth = 'max-content';
}
});
}
});
</script>
<!-- Modale di conferma per l'esportazione -->