v0.13.0 - Add Consumption Sheets module

New module for tracking production consumption with lot scanning:
- Admin configuration for process types (AD WIP, etc.)
- Dynamic table creation per process
- Flexible header/detail field definitions with Excel cell mapping
- Duplicate detection with configurable key field
- Staff scanning interface with duplicate warnings (same session/cross session)
- Excel export using uploaded templates with multi-page support
- Template settings for rows per page and detail start row
This commit is contained in:
Javier
2026-01-29 12:33:34 -06:00
parent b11421a8f5
commit d955a13f3d
11 changed files with 697 additions and 402 deletions

View File

@@ -19,36 +19,33 @@
</div>
</div>
<!-- Scan Input Card -->
{% if not dup_key_field %}
<div class="alert alert-warning" style="margin: var(--space-lg) 0; padding: var(--space-lg); background: rgba(255,170,0,0.2); border-radius: var(--radius-md);">
<strong>⚠️ No duplicate key configured!</strong><br>
Please configure a detail field with "Use for duplicate detection" checked in the admin panel.
</div>
{% endif %}
<div class="scan-card scan-card-active">
<div class="scan-header">
<h2 class="scan-title">Scan Lot Number</h2>
<h2 class="scan-title">Scan {{ dup_key_field.field_label if dup_key_field else 'Item' }}</h2>
</div>
<form id="lotScanForm" class="scan-form">
<div class="scan-input-group">
<input type="text"
name="lot_number"
id="lotInput"
inputmode="none"
class="scan-input"
placeholder="Scan Lot Number"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
autofocus>
<input type="text" name="dup_key_input" id="dupKeyInput" inputmode="none"
class="scan-input" placeholder="Scan {{ dup_key_field.field_label if dup_key_field else 'Item' }}"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus
{% if not dup_key_field %}disabled{% endif %}>
<button type="submit" style="display: none;"></button>
</div>
</form>
</div>
<!-- Duplicate Confirmation Modal (Same Session - Blue) -->
<div id="duplicateSameModal" class="modal">
<div class="modal-content modal-duplicate">
<div class="duplicate-lot-number" id="dupSameLotNumber"></div>
<h3 class="duplicate-title" style="color: var(--color-duplicate);">Already Scanned</h3>
<p class="duplicate-message">This lot was already scanned in this session.<br>Add it again?</p>
<p class="duplicate-message">This was already scanned in this session.<br>Add it again?</p>
<div class="modal-actions">
<button type="button" class="btn btn-secondary btn-lg" onclick="cancelDuplicate()">No</button>
<button type="button" class="btn btn-lg" style="background: var(--color-duplicate); color: white;" onclick="confirmDuplicate()">Yes, Add</button>
@@ -56,7 +53,6 @@
</div>
</div>
<!-- Duplicate Confirmation Modal (Other Session - Orange) -->
<div id="duplicateOtherModal" class="modal">
<div class="modal-content modal-duplicate">
<div class="duplicate-lot-number" id="dupOtherLotNumber"></div>
@@ -70,66 +66,57 @@
</div>
</div>
<!-- Weight Entry Modal -->
<div id="weightModal" class="modal">
<div id="fieldsModal" class="modal">
<div class="modal-content">
<h3 class="modal-title">Enter Weight</h3>
<h3 class="modal-title">Enter Details</h3>
<div class="modal-lot-info">
<div id="modalLotNumber" class="modal-lot-number"></div>
<div id="modalDupKeyValue" class="modal-lot-number"></div>
</div>
<form id="weightForm" class="weight-form">
<form id="fieldsForm" class="weight-form">
{% for field in detail_fields %}
{% if not field.is_duplicate_key %}
<div class="form-group">
<label class="form-label">Item Number (optional)</label>
<input type="text" id="itemInput" class="form-input" placeholder="Item/SKU">
<label class="form-label">{{ field.field_label }}{% if field.is_required %}<span style="color: var(--color-danger);">*</span>{% endif %}</label>
{% if field.field_type == 'REAL' %}
<input type="number" id="field_{{ field.field_name }}" name="{{ field.field_name }}" class="form-input" step="0.01" inputmode="decimal" {% if field.is_required %}required{% endif %}>
{% elif field.field_type == 'INTEGER' %}
<input type="number" id="field_{{ field.field_name }}" name="{{ field.field_name }}" class="form-input" step="1" {% if field.is_required %}required{% endif %}>
{% elif field.field_type == 'DATE' %}
<input type="date" id="field_{{ field.field_name }}" name="{{ field.field_name }}" class="form-input" {% if field.is_required %}required{% endif %}>
{% else %}
<input type="text" id="field_{{ field.field_name }}" name="{{ field.field_name }}" class="form-input" {% if field.max_length %}maxlength="{{ field.max_length }}"{% endif %} {% if field.is_required %}required{% endif %}>
{% endif %}
</div>
<input
type="number"
id="weightInput"
class="weight-input"
placeholder="Weight (lbs)"
step="0.01"
min="0"
inputmode="decimal"
autocomplete="off"
required
>
{% endif %}
{% endfor %}
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="cancelWeight()">Cancel</button>
<button type="button" class="btn btn-secondary" onclick="cancelFields()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- Scanned Lots Grid -->
<div class="scans-section">
<div class="scans-header">
<h3 class="scans-title">Scanned Lots (<span id="scanListCount">{{ scans|length }}</span>)</h3>
<h3 class="scans-title">Scanned Items (<span id="scanListCount">{{ scans|length }}</span>)</h3>
</div>
<div id="scansList" class="scans-grid">
{% for scan in scans %}
<div class="scan-row scan-row-{{ scan.duplicate_status }}"
data-detail-id="{{ scan.id }}"
onclick="openScanDetail({{ scan.id }})">
<div class="scan-row-lot">{{ scan.lot_number }}</div>
<div class="scan-row-item">{{ scan.item_number or 'N/A' }}</div>
<div class="scan-row-weight">{{ '%.1f'|format(scan.weight) if scan.weight else '-' }} lbs</div>
<div class="scan-row scan-row-{{ scan.duplicate_status }}" data-detail-id="{{ scan.id }}" onclick="openScanDetail({{ scan.id }})">
{% for field in detail_fields %}
<div class="scan-row-cell">{% if field.field_type == 'REAL' %}{{ '%.1f'|format(scan[field.field_name]|float) if scan[field.field_name] else '-' }}{% else %}{{ scan[field.field_name] or '-' }}{% endif %}</div>
{% endfor %}
<div class="scan-row-status">
{% if scan.duplicate_status == 'dup_same_session' %}
<span class="status-dot status-dot-blue"></span> Duplicate
{% elif scan.duplicate_status == 'dup_other_session' %}
<span class="status-dot status-dot-orange"></span> Warning
{% else %}
<span class="status-dot status-dot-green"></span> OK
{% endif %}
{% if scan.duplicate_status == 'dup_same_session' %}<span class="status-dot status-dot-blue"></span> Dup
{% elif scan.duplicate_status == 'dup_other_session' %}<span class="status-dot status-dot-orange"></span> Warn
{% else %}<span class="status-dot status-dot-green"></span> OK{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Scan Detail/Edit Modal -->
<div id="scanDetailModal" class="modal">
<div class="modal-content modal-large">
<div class="modal-header-bar">
@@ -140,257 +127,179 @@
</div>
</div>
<!-- Action Buttons -->
<div class="finish-section">
<div class="action-buttons-row">
<a href="{{ url_for('cons_sheets.index') }}" class="btn btn-secondary btn-block btn-lg">
← Back to Sessions
</a>
<button class="btn btn-success btn-block btn-lg" onclick="exportToExcel()">
📊 Export to Excel
</button>
<a href="{{ url_for('cons_sheets.index') }}" class="btn btn-secondary btn-block btn-lg">← Back to Sessions</a>
<button class="btn btn-success btn-block btn-lg" onclick="exportToExcel()">📊 Export to Excel</button>
</div>
</div>
</div>
<style>
.header-values {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
margin: var(--space-sm) 0;
}
.header-pill {
background: var(--color-surface-elevated);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
font-size: 0.8rem;
color: var(--color-text-muted);
}
.header-pill strong {
color: var(--color-text);
}
.scan-row-dup_same_session {
border-left: 4px solid var(--color-duplicate) !important;
background: rgba(0, 163, 255, 0.1) !important;
}
.scan-row-dup_other_session {
border-left: 4px solid var(--color-warning) !important;
background: rgba(255, 170, 0, 0.1) !important;
}
.scan-row-normal {
border-left: 4px solid var(--color-success);
}
.modal-duplicate {
text-align: center;
padding: var(--space-xl);
}
.duplicate-lot-number {
font-family: var(--font-mono);
font-size: 1.5rem;
font-weight: 700;
color: var(--color-primary);
margin-bottom: var(--space-md);
}
.duplicate-title {
font-size: 1.25rem;
margin-bottom: var(--space-sm);
}
.duplicate-message {
color: var(--color-text-muted);
margin-bottom: var(--space-lg);
}
.header-values { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin: var(--space-sm) 0; }
.header-pill { background: var(--color-surface-elevated); padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--color-text-muted); }
.header-pill strong { color: var(--color-text); }
.scan-row { display: grid; grid-template-columns: repeat({{ detail_fields|length }}, 1fr) auto; gap: var(--space-sm); padding: var(--space-md); background: var(--color-surface); border: 2px solid var(--color-border); border-radius: var(--radius-md); margin-bottom: var(--space-sm); cursor: pointer; transition: var(--transition); }
.scan-row:hover { border-color: var(--color-primary); }
.scan-row-cell { font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scan-row-dup_same_session { border-left: 4px solid var(--color-duplicate) !important; background: rgba(0, 163, 255, 0.1) !important; }
.scan-row-dup_other_session { border-left: 4px solid var(--color-warning) !important; background: rgba(255, 170, 0, 0.1) !important; }
.scan-row-normal { border-left: 4px solid var(--color-success); }
.modal-duplicate { text-align: center; padding: var(--space-xl); }
.duplicate-lot-number { font-family: var(--font-mono); font-size: 1.5rem; font-weight: 700; color: var(--color-primary); margin-bottom: var(--space-md); }
.duplicate-title { font-size: 1.25rem; margin-bottom: var(--space-sm); }
.duplicate-message { color: var(--color-text-muted); margin-bottom: var(--space-lg); }
</style>
<script>
let currentLotNumber = '';
const detailFields = {{ detail_fields|tojson|safe }};
const dupKeyFieldName = {{ (dup_key_field.field_name if dup_key_field else '')|tojson|safe }};
const sessionId = {{ session.id }};
let currentDupKeyValue = '';
let currentDuplicateStatus = '';
let currentDuplicateInfo = '';
let isDuplicateConfirmed = false;
let isProcessing = false;
// Lot scan handler
document.getElementById('lotScanForm').addEventListener('submit', function(e) {
e.preventDefault();
if (isProcessing) return;
const scannedValue = document.getElementById('lotInput').value.trim();
const scannedValue = document.getElementById('dupKeyInput').value.trim();
if (!scannedValue) return;
isProcessing = true;
currentLotNumber = scannedValue;
document.getElementById('lotInput').value = '';
currentDupKeyValue = scannedValue;
document.getElementById('dupKeyInput').value = '';
checkDuplicate();
});
function checkDuplicate() {
fetch('{{ url_for("cons_sheets.scan_lot", session_id=session.id) }}', {
const fieldValues = {};
fieldValues[dupKeyFieldName] = currentDupKeyValue;
fetch(`/cons-sheets/session/${sessionId}/scan`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
lot_number: currentLotNumber,
check_only: true
})
body: JSON.stringify({ field_values: fieldValues, check_only: true })
})
.then(r => r.json())
.then(data => {
if (data.needs_confirmation) {
currentDuplicateStatus = data.duplicate_status;
currentDuplicateInfo = data.duplicate_info;
if (data.duplicate_status === 'dup_same_session') {
document.getElementById('dupSameLotNumber').textContent = currentLotNumber;
document.getElementById('dupSameLotNumber').textContent = currentDupKeyValue;
document.getElementById('duplicateSameModal').style.display = 'flex';
} else if (data.duplicate_status === 'dup_other_session') {
document.getElementById('dupOtherLotNumber').textContent = currentLotNumber;
document.getElementById('dupOtherLotNumber').textContent = currentDupKeyValue;
document.getElementById('dupOtherInfo').textContent = data.duplicate_info;
document.getElementById('duplicateOtherModal').style.display = 'flex';
}
} else {
showWeightModal();
showFieldsModal();
}
})
.catch(error => {
console.error('Error:', error);
showWeightModal();
});
.catch(error => { console.error('Error:', error); showFieldsModal(); });
}
function confirmDuplicate() {
isDuplicateConfirmed = true;
document.getElementById('duplicateSameModal').style.display = 'none';
document.getElementById('duplicateOtherModal').style.display = 'none';
showWeightModal();
showFieldsModal();
}
function cancelDuplicate() {
document.getElementById('duplicateSameModal').style.display = 'none';
document.getElementById('duplicateOtherModal').style.display = 'none';
document.getElementById('lotInput').focus();
document.getElementById('dupKeyInput').focus();
isDuplicateConfirmed = false;
isProcessing = false;
currentDuplicateStatus = '';
}
function showWeightModal() {
document.getElementById('modalLotNumber').textContent = currentLotNumber;
document.getElementById('weightModal').style.display = 'flex';
document.getElementById('itemInput').value = '';
document.getElementById('weightInput').value = '';
document.getElementById('weightInput').focus();
function showFieldsModal() {
document.getElementById('modalDupKeyValue').textContent = currentDupKeyValue;
document.getElementById('fieldsModal').style.display = 'flex';
const form = document.getElementById('fieldsForm');
form.reset();
// Focus on first required field, or first input if none required
const firstRequired = form.querySelector('input[required]');
const firstInput = form.querySelector('input:not([disabled])');
if (firstRequired) firstRequired.focus();
else if (firstInput) firstInput.focus();
}
function cancelWeight() {
document.getElementById('weightModal').style.display = 'none';
document.getElementById('lotInput').focus();
function cancelFields() {
document.getElementById('fieldsModal').style.display = 'none';
document.getElementById('dupKeyInput').focus();
isDuplicateConfirmed = false;
isProcessing = false;
currentDuplicateStatus = '';
}
// Weight form handler
document.getElementById('weightForm').addEventListener('submit', function(e) {
document.getElementById('fieldsForm').addEventListener('submit', function(e) {
e.preventDefault();
const weight = document.getElementById('weightInput').value;
const itemNumber = document.getElementById('itemInput').value.trim();
if (!weight || weight <= 0) {
alert('Please enter a valid weight');
return;
}
submitScan(itemNumber, weight);
submitScan();
});
function submitScan(itemNumber, weight) {
fetch('{{ url_for("cons_sheets.scan_lot", session_id=session.id) }}', {
function submitScan() {
const fieldValues = {};
fieldValues[dupKeyFieldName] = currentDupKeyValue;
detailFields.forEach(field => {
if (!field.is_duplicate_key) {
const input = document.getElementById('field_' + field.field_name);
if (input) fieldValues[field.field_name] = input.value;
}
});
fetch(`/cons-sheets/session/${sessionId}/scan`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
lot_number: currentLotNumber,
item_number: itemNumber,
weight: parseFloat(weight),
confirm_duplicate: isDuplicateConfirmed
})
body: JSON.stringify({ field_values: fieldValues, confirm_duplicate: isDuplicateConfirmed })
})
.then(r => r.json())
.then(data => {
if (data.success) {
document.getElementById('weightModal').style.display = 'none';
// Update existing rows if needed
document.getElementById('fieldsModal').style.display = 'none';
if (data.updated_entry_ids && data.updated_entry_ids.length > 0) {
data.updated_entry_ids.forEach(id => {
const row = document.querySelector(`[data-detail-id="${id}"]`);
if (row) {
row.className = 'scan-row scan-row-dup_same_session';
row.querySelector('.scan-row-status').innerHTML = '<span class="status-dot status-dot-blue"></span> Duplicate';
row.querySelector('.scan-row-status').innerHTML = '<span class="status-dot status-dot-blue"></span> Dup';
}
});
}
// Add new row
addScanToList(data.detail_id, currentLotNumber, itemNumber, weight, data.duplicate_status);
// Reset state
currentLotNumber = '';
addScanToList(data.detail_id, fieldValues, data.duplicate_status);
currentDupKeyValue = '';
isDuplicateConfirmed = false;
isProcessing = false;
currentDuplicateStatus = '';
document.getElementById('lotInput').focus();
document.getElementById('dupKeyInput').focus();
} else {
alert(data.message || 'Error saving scan');
isProcessing = false;
}
})
.catch(error => {
console.error('Error:', error);
alert('Error saving scan');
isProcessing = false;
});
.catch(error => { console.error('Error:', error); alert('Error saving scan'); isProcessing = false; });
}
function addScanToList(detailId, lotNumber, itemNumber, weight, duplicateStatus) {
function addScanToList(detailId, fieldValues, duplicateStatus) {
const scansList = document.getElementById('scansList');
let statusClass = duplicateStatus || 'normal';
let statusDot = 'green';
let statusText = 'OK';
if (duplicateStatus === 'dup_same_session') {
statusDot = 'blue';
statusText = 'Duplicate';
} else if (duplicateStatus === 'dup_other_session') {
statusDot = 'orange';
statusText = 'Warning';
}
let statusDot = 'green', statusText = 'OK';
if (duplicateStatus === 'dup_same_session') { statusDot = 'blue'; statusText = 'Dup'; }
else if (duplicateStatus === 'dup_other_session') { statusDot = 'orange'; statusText = 'Warn'; }
const scanRow = document.createElement('div');
scanRow.className = 'scan-row scan-row-' + statusClass;
scanRow.setAttribute('data-detail-id', detailId);
scanRow.onclick = function() { openScanDetail(detailId); };
scanRow.innerHTML = `
<div class="scan-row-lot">${lotNumber}</div>
<div class="scan-row-item">${itemNumber || 'N/A'}</div>
<div class="scan-row-weight">${parseFloat(weight).toFixed(1)} lbs</div>
<div class="scan-row-status">
<span class="status-dot status-dot-${statusDot}"></span> ${statusText}
</div>
`;
let cellsHtml = '';
detailFields.forEach(field => {
let value = fieldValues[field.field_name] || '-';
if (field.field_type === 'REAL' && value !== '-') value = parseFloat(value).toFixed(1);
cellsHtml += `<div class="scan-row-cell">${value}</div>`;
});
cellsHtml += `<div class="scan-row-status"><span class="status-dot status-dot-${statusDot}"></span> ${statusText}</div>`;
scanRow.innerHTML = cellsHtml;
scansList.insertBefore(scanRow, scansList.firstChild);
// Update count
const countSpan = document.getElementById('scanListCount');
const scanCountSpan = document.getElementById('scanCount');
const newCount = scansList.children.length;
@@ -399,137 +308,97 @@ function addScanToList(detailId, lotNumber, itemNumber, weight, duplicateStatus)
}
function openScanDetail(detailId) {
fetch('/cons-sheets/detail/' + detailId)
fetch(`/cons-sheets/session/${sessionId}/detail/${detailId}`)
.then(r => r.json())
.then(data => {
if (data.success) {
displayScanDetail(data.detail);
} else {
alert('Error loading details');
}
if (data.success) displayScanDetail(data.detail);
else alert('Error loading details');
});
}
function displayScanDetail(detail) {
const content = document.getElementById('scanDetailContent');
let statusBadge = '<span class="badge badge-success">✓ OK</span>';
if (detail.duplicate_status === 'dup_same_session') {
statusBadge = '<span class="badge badge-duplicate">🔵 Duplicate (Same Session)</span>';
} else if (detail.duplicate_status === 'dup_other_session') {
statusBadge = '<span class="badge badge-warning">🟠 Previously Consumed</span>';
}
if (detail.duplicate_status === 'dup_same_session') statusBadge = '<span class="badge badge-duplicate">🔵 Duplicate</span>';
else if (detail.duplicate_status === 'dup_other_session') statusBadge = '<span class="badge badge-warning">🟠 Previously Consumed</span>';
let detailRows = '';
detailFields.forEach(field => {
let value = detail[field.field_name] || 'N/A';
if (field.field_type === 'REAL' && value !== 'N/A') value = parseFloat(value).toFixed(2);
detailRows += `<div class="detail-row"><span class="detail-label">${field.field_label}:</span><span class="detail-value">${value}</span></div>`;
});
let editFields = '';
detailFields.forEach(field => {
let inputType = 'text', extraAttrs = '';
if (field.field_type === 'REAL') { inputType = 'number'; extraAttrs = 'step="0.01"'; }
else if (field.field_type === 'INTEGER') { inputType = 'number'; extraAttrs = 'step="1"'; }
else if (field.field_type === 'DATE') { inputType = 'date'; }
let value = detail[field.field_name] || '';
editFields += `<div class="form-group"><label class="form-label">${field.field_label}</label><input type="${inputType}" id="edit_${field.field_name}" class="form-input" value="${value}" ${extraAttrs}></div>`;
});
content.innerHTML = `
<div class="detail-section">
<div class="detail-row">
<span class="detail-label">Lot Number:</span>
<span class="detail-value detail-lot">${detail.lot_number}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status:</span>
<span class="detail-value">${statusBadge}</span>
</div>
<div class="detail-section">${detailRows}
<div class="detail-row"><span class="detail-label">Status:</span><span class="detail-value">${statusBadge}</span></div>
${detail.duplicate_info ? `<div class="detail-row"><span class="detail-label">Info:</span><span class="detail-value">${detail.duplicate_info}</span></div>` : ''}
<div class="detail-row">
<span class="detail-label">Scanned:</span>
<span class="detail-value">${detail.scanned_at} by ${detail.scanned_by_name}</span>
</div>
</div>
<div class="detail-section">
<h4 class="detail-section-title">Edit Scan</h4>
<div class="detail-form">
<div class="form-group">
<label class="form-label">Item Number</label>
<input type="text" id="editItem" class="form-input" value="${detail.item_number || ''}">
</div>
<div class="form-group">
<label class="form-label">Lot Number</label>
<input type="text" id="editLot" class="form-input" value="${detail.lot_number || ''}">
</div>
<div class="form-group">
<label class="form-label">Weight (lbs)</label>
<input type="number" id="editWeight" class="form-input" value="${detail.weight || ''}" step="0.01" min="0">
</div>
<div class="form-group">
<label class="form-label">Comment</label>
<textarea id="editComment" class="form-textarea" rows="3">${detail.comment || ''}</textarea>
</div>
</div>
<div class="detail-row"><span class="detail-label">Scanned:</span><span class="detail-value">${detail.scanned_at} by ${detail.scanned_by_name}</span></div>
</div>
<div class="detail-section"><h4 class="detail-section-title">Edit Scan</h4><div class="detail-form">${editFields}
<div class="form-group"><label class="form-label">Comment</label><textarea id="editComment" class="form-textarea" rows="3">${detail.comment || ''}</textarea></div>
</div></div>
<div class="detail-actions">
<button class="btn btn-secondary" onclick="closeScanDetail()">Cancel</button>
<button class="btn btn-danger" onclick="deleteDetail(${detail.id})">Delete</button>
<button class="btn btn-primary" onclick="saveDetail(${detail.id})">Save Changes</button>
</div>
`;
</div>`;
document.getElementById('scanDetailModal').style.display = 'flex';
}
function closeScanDetail() {
document.getElementById('scanDetailModal').style.display = 'none';
}
function closeScanDetail() { document.getElementById('scanDetailModal').style.display = 'none'; }
function saveDetail(detailId) {
const item = document.getElementById('editItem').value.trim();
const lot = document.getElementById('editLot').value.trim();
const weight = document.getElementById('editWeight').value;
const fieldValues = {};
detailFields.forEach(field => {
const input = document.getElementById('edit_' + field.field_name);
if (input) fieldValues[field.field_name] = input.value;
});
const comment = document.getElementById('editComment').value;
if (!lot) {
alert('Lot number is required');
return;
}
fetch('/cons-sheets/detail/' + detailId + '/update', {
fetch(`/cons-sheets/session/${sessionId}/detail/${detailId}/update`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ item_number: item, lot_number: lot, weight: parseFloat(weight), comment: comment })
body: JSON.stringify({ field_values: fieldValues, comment: comment })
})
.then(r => r.json())
.then(data => {
if (data.success) {
closeScanDetail();
location.reload();
} else {
alert(data.message || 'Error updating');
}
if (data.success) { closeScanDetail(); location.reload(); }
else alert(data.message || 'Error updating');
});
}
function deleteDetail(detailId) {
if (!confirm('Delete this scan?')) return;
fetch('/cons-sheets/detail/' + detailId + '/delete', {
fetch(`/cons-sheets/session/${sessionId}/detail/${detailId}/delete`, {
method: 'POST',
headers: {'Content-Type': 'application/json'}
})
.then(r => r.json())
.then(data => {
if (data.success) {
closeScanDetail();
location.reload();
} else {
alert(data.message || 'Error deleting');
}
if (data.success) { closeScanDetail(); location.reload(); }
else alert(data.message || 'Error deleting');
});
}
function exportToExcel() {
alert('Excel export coming soon! For now, you can view and print the data from the admin panel.');
// TODO: Implement Excel export
function exportToExcel() {
window.location.href = `/cons-sheets/session/${sessionId}/export?format=xlsx`;
}
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.getElementById('weightModal').style.display = 'none';
document.getElementById('fieldsModal').style.display = 'none';
document.getElementById('duplicateSameModal').style.display = 'none';
document.getElementById('duplicateOtherModal').style.display = 'none';
document.getElementById('scanDetailModal').style.display = 'none';
document.getElementById('lotInput').focus();
document.getElementById('dupKeyInput').focus();
}
});
</script>
{% endblock %}
{% endblock %}