Compare commits
2 Commits
feature/ar
...
90f30db915
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f30db915 | ||
|
|
4e7d88c3f9 |
2
app.py
2
app.py
@@ -28,7 +28,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
|
|||||||
|
|
||||||
|
|
||||||
# 1. Define the version
|
# 1. Define the version
|
||||||
APP_VERSION = '0.18.2'
|
APP_VERSION = '0.18.3'
|
||||||
|
|
||||||
# 2. Inject it into all templates automatically
|
# 2. Inject it into all templates automatically
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
|
|||||||
@@ -773,6 +773,91 @@ def register_routes(bp):
|
|||||||
header_fields=header_fields,
|
header_fields=header_fields,
|
||||||
form_data={})
|
form_data={})
|
||||||
|
|
||||||
|
@bp.route('/session/<int:session_id>/edit-header', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def edit_session_header(session_id):
|
||||||
|
"""Edit header fields for an existing session"""
|
||||||
|
sess = query_db('''
|
||||||
|
SELECT cs.*, cp.process_name, cp.process_key, cp.id as process_id
|
||||||
|
FROM cons_sessions cs
|
||||||
|
JOIN cons_processes cp ON cs.process_id = cp.id
|
||||||
|
WHERE cs.id = ?
|
||||||
|
''', [session_id], one=True)
|
||||||
|
|
||||||
|
if not sess:
|
||||||
|
flash('Session not found', 'danger')
|
||||||
|
return redirect(url_for('conssheets.index'))
|
||||||
|
|
||||||
|
# Check permission
|
||||||
|
if sess['created_by'] != session['user_id'] and session['role'] not in ['owner', 'admin']:
|
||||||
|
flash('Permission denied', 'danger')
|
||||||
|
return redirect(url_for('conssheets.index'))
|
||||||
|
|
||||||
|
# Get header fields
|
||||||
|
header_fields = query_db('''
|
||||||
|
SELECT * FROM cons_process_fields
|
||||||
|
WHERE process_id = ? AND table_type = 'header' AND is_active = 1
|
||||||
|
ORDER BY sort_order, id
|
||||||
|
''', [sess['process_id']])
|
||||||
|
|
||||||
|
# Get existing values
|
||||||
|
existing_values = query_db('''
|
||||||
|
SELECT cpf.field_name, cshv.field_value
|
||||||
|
FROM cons_process_fields cpf
|
||||||
|
LEFT JOIN cons_session_header_values cshv
|
||||||
|
ON cpf.id = cshv.field_id AND cshv.session_id = ?
|
||||||
|
WHERE cpf.process_id = ? AND cpf.table_type = 'header' AND cpf.is_active = 1
|
||||||
|
''', [session_id, sess['process_id']])
|
||||||
|
|
||||||
|
# Build form_data dict from existing values
|
||||||
|
form_data = {row['field_name']: row['field_value'] for row in existing_values if row['field_value']}
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Validate required fields
|
||||||
|
missing_required = []
|
||||||
|
for field in header_fields:
|
||||||
|
if field['is_required']:
|
||||||
|
value = request.form.get(field['field_name'], '').strip()
|
||||||
|
if not value:
|
||||||
|
missing_required.append(field['field_label'])
|
||||||
|
|
||||||
|
if missing_required:
|
||||||
|
flash(f'Required fields missing: {", ".join(missing_required)}', 'danger')
|
||||||
|
return render_template('conssheets/new_session.html',
|
||||||
|
process=sess,
|
||||||
|
header_fields=header_fields,
|
||||||
|
form_data=request.form,
|
||||||
|
edit_mode=True,
|
||||||
|
session_id=session_id)
|
||||||
|
|
||||||
|
# Update header field values
|
||||||
|
for field in header_fields:
|
||||||
|
value = request.form.get(field['field_name'], '').strip()
|
||||||
|
existing = query_db(
|
||||||
|
'SELECT id FROM cons_session_header_values WHERE session_id = ? AND field_id = ?',
|
||||||
|
[session_id, field['id']], one=True
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
execute_db(
|
||||||
|
'UPDATE cons_session_header_values SET field_value = ? WHERE session_id = ? AND field_id = ?',
|
||||||
|
[value, session_id, field['id']]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if value:
|
||||||
|
execute_db(
|
||||||
|
'INSERT INTO cons_session_header_values (session_id, field_id, field_value) VALUES (?, ?, ?)',
|
||||||
|
[session_id, field['id'], value]
|
||||||
|
)
|
||||||
|
|
||||||
|
flash('Header updated successfully!', 'success')
|
||||||
|
return redirect(url_for('conssheets.scan_session', session_id=session_id))
|
||||||
|
|
||||||
|
return render_template('conssheets/new_session.html',
|
||||||
|
process=sess,
|
||||||
|
header_fields=header_fields,
|
||||||
|
form_data=form_data,
|
||||||
|
edit_mode=True,
|
||||||
|
session_id=session_id)
|
||||||
|
|
||||||
@bp.route('/session/<int:session_id>')
|
@bp.route('/session/<int:session_id>')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -833,6 +918,41 @@ def register_routes(bp):
|
|||||||
dup_key_field=dup_key_field)
|
dup_key_field=dup_key_field)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/session/<int:session_id>/update-header', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_session_header(session_id):
|
||||||
|
"""Update header field values for a session"""
|
||||||
|
sess = query_db('SELECT * FROM cons_sessions WHERE id = ?', [session_id], one=True)
|
||||||
|
|
||||||
|
if not sess:
|
||||||
|
return jsonify({'success': False, 'message': 'Session not found'})
|
||||||
|
|
||||||
|
# Check permission
|
||||||
|
if sess['created_by'] != session['user_id'] and session['role'] not in ['owner', 'admin']:
|
||||||
|
return jsonify({'success': False, 'message': 'Permission denied'})
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
field_values = data.get('field_values', {})
|
||||||
|
|
||||||
|
for field_id, field_value in field_values.items():
|
||||||
|
# Check if value already exists
|
||||||
|
existing = query_db(
|
||||||
|
'SELECT id FROM cons_session_header_values WHERE session_id = ? AND field_id = ?',
|
||||||
|
[session_id, field_id], one=True
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
execute_db(
|
||||||
|
'UPDATE cons_session_header_values SET field_value = ? WHERE session_id = ? AND field_id = ?',
|
||||||
|
[field_value, session_id, field_id]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
execute_db(
|
||||||
|
'INSERT INTO cons_session_header_values (session_id, field_id, field_value) VALUES (?, ?, ?)',
|
||||||
|
[session_id, field_id, field_value]
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
@bp.route('/session/<int:session_id>/scan', methods=['POST'])
|
@bp.route('/session/<int:session_id>/scan', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def scan_lot(session_id):
|
def scan_lot(session_id):
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-container" style="max-width: 600px; margin: 0 auto;">
|
<div class="form-container" style="max-width: 600px; margin: 0 auto;">
|
||||||
<h1 class="page-title" style="text-align: center;">New {{ process.process_name }} Session</h1>
|
<h1 class="page-title" style="text-align: center;">{% if edit_mode %}Edit{% else %}New{% endif %} {{ process.process_name }} Session</h1>
|
||||||
<p class="page-subtitle" style="text-align: center; margin-bottom: var(--space-xl);">Enter header information to begin scanning</p>
|
<p class="page-subtitle" style="text-align: center; margin-bottom: var(--space-xl);">{% if edit_mode %}Update header information{% else %}Enter header information to begin scanning{% endif %}</p>
|
||||||
|
|
||||||
<form method="POST" class="form-card">
|
<form method="POST" action="{% if edit_mode %}{{ url_for('conssheets.edit_session_header', session_id=session_id) }}{% endif %}" class="form-card">
|
||||||
{% for field in header_fields %}
|
{% for field in header_fields %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ field.field_name }}" class="form-label">
|
<label for="{{ field.field_name }}" class="form-label">
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a href="{{ url_for('conssheets.index') }}" class="btn btn-secondary">Cancel</a>
|
<a href="{{ url_for('conssheets.index') }}" class="btn btn-secondary">Cancel</a>
|
||||||
<button type="submit" class="btn btn-primary" {% if not header_fields %}disabled{% endif %}>
|
<button type="submit" class="btn btn-primary" {% if not header_fields %}disabled{% endif %}>
|
||||||
Start Scanning
|
{% if edit_mode %}Save Changes{% else %}Start Scanning{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,7 +6,13 @@
|
|||||||
<div class="count-location-container">
|
<div class="count-location-container">
|
||||||
<div class="location-header">
|
<div class="location-header">
|
||||||
<div class="location-info">
|
<div class="location-info">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<a href="{{ url_for('conssheets.index') }}" class="breadcrumb">← Back to Sessions</a>
|
<a href="{{ url_for('conssheets.index') }}" class="breadcrumb">← Back to Sessions</a>
|
||||||
|
<a href="{{ url_for('conssheets.edit_session_header', session_id=session.id) }}"
|
||||||
|
class="btn-edit-header" title="Edit header fields">
|
||||||
|
<i class="fa-solid fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="location-label">{{ session.process_name }}</div>
|
<div class="location-label">{{ session.process_name }}</div>
|
||||||
<div class="header-values">
|
<div class="header-values">
|
||||||
{% for hv in header_values %}
|
{% for hv in header_values %}
|
||||||
@@ -27,7 +33,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="scan-card" style="border: 2px solid var(--color-primary); margin-bottom: 20px;">
|
<div class="scan-card" style="border: 2px solid var(--color-primary); margin-bottom: 20px;">
|
||||||
<div class="scan-header" style="background: rgba(0, 123, 255, 0.1);">
|
<div class="scan-header" >
|
||||||
<h2 class="scan-title" style="color: var(--color-primary);">🚀 Smart Router Scan</h2>
|
<h2 class="scan-title" style="color: var(--color-primary);">🚀 Smart Router Scan</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="scan-form">
|
<div class="scan-form">
|
||||||
@@ -111,19 +117,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="scans-grid scan-header-row" style="--field-count: {{ detail_fields|length }};">
|
<div class="scans-grid scan-header-row" style="--field-count: {{ detail_fields|length }};">
|
||||||
{% for field in detail_fields %}
|
{% for field in detail_fields %}
|
||||||
<div class="scan-row-cell scan-col-header">{{ field.field_label }}</div>
|
<div class="cs-scan-row-cell scan-col-header">{{ field.field_label }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="scan-row-cell scan-col-header">Status</div>
|
<div class="cs-scan-row-cell scan-col-header">Status</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="scansList" class="scans-grid" style="--field-count: {{ detail_fields|length }};">
|
<div id="scansList" class="scans-grid" style="--field-count: {{ detail_fields|length }};">
|
||||||
{% for scan in scans %}
|
{% for scan in scans %}
|
||||||
<div class="scan-row scan-row-{{ scan.duplicate_status }}"
|
<div class="cs-scan-row cs-scan-row-{{ scan.duplicate_status }}"
|
||||||
data-detail-id="{{ scan.id }}"
|
data-detail-id="{{ scan.id }}"
|
||||||
onclick="openScanDetail(this.dataset.detailId)">
|
onclick="openScanDetail(this.dataset.detailId)">
|
||||||
{% for field in detail_fields %}
|
{% 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>
|
<div class="cs-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 %}
|
{% endfor %}
|
||||||
<div class="scan-row-status">
|
<div class="cs-scan-row-status">
|
||||||
{% if scan.duplicate_status == 'dup_same_session' %}<span class="status-dot status-dot-blue"></span> Dup
|
{% 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
|
{% 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 %}
|
{% else %}<span class="status-dot status-dot-green"></span> OK{% endif %}
|
||||||
@@ -184,21 +190,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<!-- Styles moved to static/css/style.css -->
|
||||||
.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(var(--field-count), 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 id="session-data" type="application/json">
|
<script id="session-data" type="application/json">
|
||||||
{
|
{
|
||||||
@@ -319,7 +311,10 @@ function processSmartScan(barcode, confirm = false) {
|
|||||||
feedbackArea.style.border = '1px solid #dc3545';
|
feedbackArea.style.border = '1px solid #dc3545';
|
||||||
feedbackText.style.color = '#dc3545';
|
feedbackText.style.color = '#dc3545';
|
||||||
feedbackText.textContent = data.message;
|
feedbackText.textContent = data.message;
|
||||||
if(smartInput) smartInput.focus();
|
if(smartInput) {
|
||||||
|
smartInput.value = '';
|
||||||
|
smartInput.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -529,8 +524,8 @@ function submitScan() {
|
|||||||
data.updated_entry_ids.forEach(id => {
|
data.updated_entry_ids.forEach(id => {
|
||||||
const row = document.querySelector(`[data-detail-id="${id}"]`);
|
const row = document.querySelector(`[data-detail-id="${id}"]`);
|
||||||
if (row) {
|
if (row) {
|
||||||
row.className = 'scan-row scan-row-dup_same_session';
|
row.className = 'cs-scan-row cs-scan-row-dup_same_session';
|
||||||
row.querySelector('.scan-row-status').innerHTML = '<span class="status-dot status-dot-blue"></span> Dup';
|
row.querySelector('.cs-scan-row-status').innerHTML = '<span class="status-dot status-dot-blue"></span> Dup';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -555,16 +550,16 @@ function addScanToList(detailId, fieldValues, duplicateStatus) {
|
|||||||
if (duplicateStatus === 'dup_same_session') { statusDot = 'blue'; statusText = 'Dup'; }
|
if (duplicateStatus === 'dup_same_session') { statusDot = 'blue'; statusText = 'Dup'; }
|
||||||
else if (duplicateStatus === 'dup_other_session') { statusDot = 'orange'; statusText = 'Warn'; }
|
else if (duplicateStatus === 'dup_other_session') { statusDot = 'orange'; statusText = 'Warn'; }
|
||||||
const scanRow = document.createElement('div');
|
const scanRow = document.createElement('div');
|
||||||
scanRow.className = 'scan-row scan-row-' + statusClass;
|
scanRow.className = 'cs-scan-row cs-scan-row-' + statusClass;
|
||||||
scanRow.setAttribute('data-detail-id', detailId);
|
scanRow.setAttribute('data-detail-id', detailId);
|
||||||
scanRow.onclick = function() { openScanDetail(detailId); };
|
scanRow.onclick = function() { openScanDetail(detailId); };
|
||||||
let cellsHtml = '';
|
let cellsHtml = '';
|
||||||
detailFields.forEach(field => {
|
detailFields.forEach(field => {
|
||||||
let value = fieldValues[field.field_name] || '-';
|
let value = fieldValues[field.field_name] || '-';
|
||||||
if (field.field_type === 'REAL' && value !== '-') value = parseFloat(value).toFixed(1);
|
if (field.field_type === 'REAL' && value !== '-') value = parseFloat(value).toFixed(1);
|
||||||
cellsHtml += `<div class="scan-row-cell">${value}</div>`;
|
cellsHtml += `<div class="cs-scan-row-cell">${value}</div>`;
|
||||||
});
|
});
|
||||||
cellsHtml += `<div class="scan-row-status"><span class="status-dot status-dot-${statusDot}"></span> ${statusText}</div>`;
|
cellsHtml += `<div class="cs-scan-row-status"><span class="status-dot status-dot-${statusDot}"></span> ${statusText}</div>`;
|
||||||
scanRow.innerHTML = cellsHtml;
|
scanRow.innerHTML = cellsHtml;
|
||||||
scansList.insertBefore(scanRow, scansList.firstChild);
|
scansList.insertBefore(scanRow, scansList.firstChild);
|
||||||
const countSpan = document.getElementById('scanListCount');
|
const countSpan = document.getElementById('scanListCount');
|
||||||
|
|||||||
@@ -1245,6 +1245,23 @@ body {
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== SCAN SESSION ==================== */
|
||||||
|
.header-values { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin: var(--space-sm) 0; align-items: center; flex-direction: row; }
|
||||||
|
.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); }
|
||||||
|
.btn-edit-header { background: transparent; border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: var(--space-xs) var(--space-sm); color: var(--color-text-muted); cursor: pointer; font-size: 0.75rem; text-decoration: none; transition: var(--transition); display: inline-flex; align-items: center; }
|
||||||
|
.btn-edit-header:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||||
|
.cs-scan-row { display: grid; grid-template-columns: repeat(var(--field-count), 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); }
|
||||||
|
.cs-scan-row:hover { border-color: var(--color-primary); }
|
||||||
|
.cs-scan-row-cell { font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.cs-scan-row-dup_same_session { border-left: 4px solid var(--color-duplicate) !important; background: rgba(0, 163, 255, 0.1) !important; }
|
||||||
|
.cs-scan-row-dup_other_session { border-left: 4px solid var(--color-warning) !important; background: rgba(255, 170, 0, 0.1) !important; }
|
||||||
|
.cs-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); }
|
||||||
|
|
||||||
/* ==================== LOCATION COUNTING ==================== */
|
/* ==================== LOCATION COUNTING ==================== */
|
||||||
|
|
||||||
.count-location-container {
|
.count-location-container {
|
||||||
@@ -1262,7 +1279,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.location-label {
|
.location-label {
|
||||||
font-size: 0.875rem;
|
font-size: 1.5rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
|
|||||||
Reference in New Issue
Block a user