Compare commits
10 Commits
deb74fd971
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7ffa8ee0 | ||
|
|
907f805cca | ||
|
|
3d6421e0ed | ||
|
|
90f30db915 | ||
|
|
4e7d88c3f9 | ||
|
|
0df35b015b | ||
|
|
b97424554c | ||
|
|
de72a1fb9e | ||
|
|
caeefa5d61 | ||
|
|
ea403934d3 |
2
app.py
2
app.py
@@ -28,7 +28,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
|
||||
|
||||
|
||||
# 1. Define the version
|
||||
APP_VERSION = '0.18.0'
|
||||
APP_VERSION = '0.18.4'
|
||||
|
||||
# 2. Inject it into all templates automatically
|
||||
@app.context_processor
|
||||
|
||||
@@ -116,9 +116,9 @@ def execute_pipeline(actions, barcode, context):
|
||||
|
||||
placeholders = ', '.join(['?'] * len(cols))
|
||||
sql = f"INSERT INTO {context['table_name']} ({', '.join(cols)}) VALUES ({placeholders})"
|
||||
execute_db(sql, vals)
|
||||
new_id = execute_db(sql, vals)
|
||||
|
||||
return {'success': True, 'message': 'Saved Successfully', 'data': field_values}
|
||||
return {'success': True, 'message': 'Saved Successfully', 'detail_id': new_id, 'data': field_values}
|
||||
except Exception as e:
|
||||
return {'success': False, 'message': f"Database Error: {str(e)}", 'data': field_values}
|
||||
else:
|
||||
|
||||
@@ -148,10 +148,46 @@ def get_migrations():
|
||||
''')
|
||||
print(" Created cons_process_router table")
|
||||
|
||||
def migration_006_add_deleted_status(conn):
|
||||
"""Add 'deleted' to the status CHECK constraint"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# SQLite doesn't support ALTER COLUMN, so we need to recreate the table
|
||||
# First, create a new table with the updated constraint
|
||||
cursor.execute('''
|
||||
CREATE TABLE cons_sessions_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
process_id INTEGER NOT NULL,
|
||||
created_by INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived', 'deleted')),
|
||||
FOREIGN KEY (process_id) REFERENCES cons_processes(id),
|
||||
FOREIGN KEY (created_by) REFERENCES Users(user_id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Copy data from old table
|
||||
cursor.execute('''
|
||||
INSERT INTO cons_sessions_new (id, process_id, created_by, created_at, status)
|
||||
SELECT id, process_id, created_by, created_at, status
|
||||
FROM cons_sessions
|
||||
''')
|
||||
|
||||
# Drop old table and rename new one
|
||||
cursor.execute('DROP TABLE cons_sessions')
|
||||
cursor.execute('ALTER TABLE cons_sessions_new RENAME TO cons_sessions')
|
||||
|
||||
# Recreate indexes
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_sessions_process ON cons_sessions(process_id, status)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_sessions_user ON cons_sessions(created_by, status)')
|
||||
|
||||
print(" Updated cons_sessions status constraint to include 'deleted'")
|
||||
|
||||
return [
|
||||
(1, 'add_is_duplicate_key', migration_001_add_is_duplicate_key),
|
||||
(2, 'add_detail_end_row', migration_002_add_detail_end_row),
|
||||
(3, 'add_page_height', migration_003_add_page_height),
|
||||
(4, 'add_print_columns', migration_004_add_print_columns),
|
||||
(5, 'create_router_table', migration_005_create_router_table),
|
||||
(6, 'add_deleted_status', migration_006_add_deleted_status),
|
||||
]
|
||||
@@ -619,6 +619,16 @@ def register_routes(bp):
|
||||
LIMIT 1
|
||||
''', [process_id], one=True)
|
||||
|
||||
def strip_title_prefix(label):
|
||||
"""Strip [title] prefix from field label for display"""
|
||||
if label and label.startswith('[title]'):
|
||||
return label[7:]
|
||||
return label
|
||||
|
||||
def is_title_field(label):
|
||||
"""Check if field label has [title] prefix"""
|
||||
return label and label.startswith('[title]')
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
@@ -637,29 +647,86 @@ def register_routes(bp):
|
||||
flash('You do not have access to this module', 'danger')
|
||||
return redirect(url_for('home'))
|
||||
|
||||
# Get user's active sessions with process info and scan counts
|
||||
active_sessions = query_db('''
|
||||
SELECT cs.*, cp.process_name, cp.process_key
|
||||
FROM cons_sessions cs
|
||||
JOIN cons_processes cp ON cs.process_id = cp.id
|
||||
WHERE cs.created_by = ? AND cs.status = 'active'
|
||||
ORDER BY cs.created_at DESC
|
||||
''', [user_id])
|
||||
# Check if user wants to see archived/deleted sessions
|
||||
show_archived = request.args.get('show_archived') == '1'
|
||||
|
||||
# Get scan counts for each session from their dynamic tables
|
||||
# Get sessions based on filter
|
||||
if show_archived:
|
||||
# Show active + archived + last 20 deleted
|
||||
all_sessions = query_db('''
|
||||
SELECT cs.*, cp.process_name, cp.process_key
|
||||
FROM cons_sessions cs
|
||||
JOIN cons_processes cp ON cs.process_id = cp.id
|
||||
WHERE cs.created_by = ?
|
||||
ORDER BY
|
||||
CASE cs.status
|
||||
WHEN 'active' THEN 1
|
||||
WHEN 'archived' THEN 2
|
||||
WHEN 'deleted' THEN 3
|
||||
END,
|
||||
cs.created_at DESC
|
||||
''', [user_id])
|
||||
|
||||
# Separate active, archived, and deleted (limit deleted to 20)
|
||||
active_sessions = [s for s in all_sessions if s['status'] == 'active']
|
||||
archived_sessions = [s for s in all_sessions if s['status'] == 'archived']
|
||||
deleted_sessions = [s for s in all_sessions if s['status'] == 'deleted'][:20]
|
||||
|
||||
combined_sessions = active_sessions + archived_sessions + deleted_sessions
|
||||
else:
|
||||
# Show only active sessions
|
||||
combined_sessions = query_db('''
|
||||
SELECT cs.*, cp.process_name, cp.process_key
|
||||
FROM cons_sessions cs
|
||||
JOIN cons_processes cp ON cs.process_id = cp.id
|
||||
WHERE cs.created_by = ? AND cs.status = 'active'
|
||||
ORDER BY cs.created_at DESC
|
||||
''', [user_id])
|
||||
|
||||
# Get scan counts and header values for each session
|
||||
sessions_with_counts = []
|
||||
for sess in active_sessions:
|
||||
for sess in combined_sessions:
|
||||
sess_dict = dict(sess)
|
||||
|
||||
# Get scan count
|
||||
table_name = get_detail_table_name(sess['process_key'])
|
||||
try:
|
||||
count_result = query_db(f'''
|
||||
SELECT COUNT(*) as scan_count FROM {table_name}
|
||||
WHERE session_id = ? AND is_deleted = 0
|
||||
''', [sess['id']], one=True)
|
||||
sess_dict = dict(sess)
|
||||
sess_dict['scan_count'] = count_result['scan_count'] if count_result else 0
|
||||
except:
|
||||
sess_dict = dict(sess)
|
||||
sess_dict['scan_count'] = 0
|
||||
|
||||
# Get header fields with their values
|
||||
header_fields = query_db('''
|
||||
SELECT cpf.field_label, 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
|
||||
ORDER BY cpf.sort_order, cpf.id
|
||||
''', [sess['id'], sess['process_id']])
|
||||
|
||||
# Build title from [title] fields, strip prefix for display
|
||||
title_parts = []
|
||||
preview_fields = []
|
||||
for hf in header_fields:
|
||||
hf_dict = dict(hf)
|
||||
if is_title_field(hf_dict['field_label']):
|
||||
clean_label = strip_title_prefix(hf_dict['field_label'])
|
||||
hf_dict['field_label'] = clean_label
|
||||
if hf_dict['field_value']:
|
||||
title_parts.append(f"{clean_label} {hf_dict['field_value']}")
|
||||
preview_fields.append(hf_dict)
|
||||
else:
|
||||
preview_fields.append(hf_dict)
|
||||
|
||||
sess_dict['header_preview'] = preview_fields[:5]
|
||||
sess_dict['session_title'] = f"{sess['process_name']} — {', '.join(title_parts)}" if title_parts else sess['process_name']
|
||||
sessions_with_counts.append(sess_dict)
|
||||
|
||||
# Get available process types for creating new sessions
|
||||
@@ -668,8 +735,9 @@ def register_routes(bp):
|
||||
''')
|
||||
|
||||
return render_template('conssheets/staff_index.html',
|
||||
sessions=sessions_with_counts,
|
||||
processes=processes)
|
||||
sessions=sessions_with_counts,
|
||||
processes=processes,
|
||||
show_archived=show_archived)
|
||||
|
||||
|
||||
@bp.route('/new/<int:process_id>', methods=['GET', 'POST'])
|
||||
@@ -682,13 +750,19 @@ def register_routes(bp):
|
||||
flash('Process not found', 'danger')
|
||||
return redirect(url_for('conssheets.index'))
|
||||
|
||||
# Get header fields for this process
|
||||
header_fields = query_db('''
|
||||
# Get header fields for this process, stripping [title] prefix
|
||||
header_fields_raw = query_db('''
|
||||
SELECT * FROM cons_process_fields
|
||||
WHERE process_id = ? AND table_type = 'header' AND is_active = 1
|
||||
ORDER BY sort_order, id
|
||||
''', [process_id])
|
||||
|
||||
header_fields = []
|
||||
for hf in header_fields_raw:
|
||||
hf_dict = dict(hf)
|
||||
hf_dict['field_label'] = strip_title_prefix(hf_dict['field_label'])
|
||||
header_fields.append(hf_dict)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Validate required fields
|
||||
missing_required = []
|
||||
@@ -728,6 +802,97 @@ def register_routes(bp):
|
||||
header_fields=header_fields,
|
||||
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 for this process, stripping [title] prefix
|
||||
header_fields_raw = 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']])
|
||||
|
||||
header_fields = []
|
||||
for hf in header_fields_raw:
|
||||
hf_dict = dict(hf)
|
||||
hf_dict['field_label'] = strip_title_prefix(hf_dict['field_label'])
|
||||
header_fields.append(hf_dict)
|
||||
|
||||
# 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>')
|
||||
@login_required
|
||||
@@ -749,14 +914,24 @@ def register_routes(bp):
|
||||
flash('This session has been archived', 'warning')
|
||||
return redirect(url_for('conssheets.index'))
|
||||
|
||||
# Get header values for display
|
||||
header_values = query_db('''
|
||||
SELECT cpf.field_label, cpf.field_name, cshv.field_value
|
||||
FROM cons_session_header_values cshv
|
||||
JOIN cons_process_fields cpf ON cshv.field_id = cpf.id
|
||||
WHERE cshv.session_id = ?
|
||||
# Get header values for display, stripping [title] prefix
|
||||
header_values_raw = query_db('''
|
||||
SELECT cpf.id as field_id, cpf.field_label, cpf.field_name,
|
||||
cpf.field_type, cpf.is_required, cpf.max_length,
|
||||
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
|
||||
ORDER BY cpf.sort_order, cpf.id
|
||||
''', [session_id])
|
||||
''', [session_id, sess['process_id']])
|
||||
|
||||
# Strip [title] prefix for display
|
||||
header_values = []
|
||||
for hv in header_values_raw:
|
||||
hv_dict = dict(hv)
|
||||
hv_dict['field_label'] = strip_title_prefix(hv_dict['field_label'])
|
||||
header_values.append(hv_dict)
|
||||
|
||||
# Get detail fields for this process (convert to dicts for JSON serialization)
|
||||
detail_fields_rows = query_db('''
|
||||
@@ -788,6 +963,41 @@ def register_routes(bp):
|
||||
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'])
|
||||
@login_required
|
||||
def scan_lot(session_id):
|
||||
@@ -982,6 +1192,60 @@ def register_routes(bp):
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
@bp.route('/session/<int:session_id>/unarchive', methods=['POST'])
|
||||
@login_required
|
||||
def unarchive_session(session_id):
|
||||
"""Unarchive a session back to active"""
|
||||
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'})
|
||||
|
||||
execute_db('UPDATE cons_sessions SET status = "active" WHERE id = ?', [session_id])
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/session/<int:session_id>/delete', methods=['POST'])
|
||||
@role_required('owner', 'admin')
|
||||
def delete_session(session_id):
|
||||
"""Delete a session (admin only) - soft delete session and all detail rows"""
|
||||
sess = query_db('SELECT cs.*, cp.process_key FROM cons_sessions cs JOIN cons_processes cp ON cs.process_id = cp.id WHERE cs.id = ?', [session_id], one=True)
|
||||
|
||||
if not sess:
|
||||
return jsonify({'success': False, 'message': 'Session not found'})
|
||||
|
||||
# Update session status
|
||||
execute_db('UPDATE cons_sessions SET status = "deleted" WHERE id = ?', [session_id])
|
||||
|
||||
# Mark all detail rows as deleted
|
||||
table_name = get_detail_table_name(sess['process_key'])
|
||||
execute_db(f'UPDATE {table_name} SET is_deleted = 1 WHERE session_id = ?', [session_id])
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/session/<int:session_id>/restore', methods=['POST'])
|
||||
@role_required('owner', 'admin')
|
||||
def restore_session(session_id):
|
||||
"""Restore a deleted session (admin only) - restore session and all detail rows"""
|
||||
sess = query_db('SELECT cs.*, cp.process_key FROM cons_sessions cs JOIN cons_processes cp ON cs.process_id = cp.id WHERE cs.id = ?', [session_id], one=True)
|
||||
|
||||
if not sess:
|
||||
return jsonify({'success': False, 'message': 'Session not found'})
|
||||
|
||||
# Update session status to active
|
||||
execute_db('UPDATE cons_sessions SET status = "active" WHERE id = ?', [session_id])
|
||||
|
||||
# Restore all detail rows
|
||||
table_name = get_detail_table_name(sess['process_key'])
|
||||
execute_db(f'UPDATE {table_name} SET is_deleted = 0 WHERE session_id = ?', [session_id])
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
@bp.route('/session/<int:session_id>/template')
|
||||
@login_required
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p class="page-subtitle" style="text-align: center; margin-bottom: var(--space-xl);">Enter header information to begin scanning</p>
|
||||
<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);">{% 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 %}
|
||||
<div class="form-group">
|
||||
<label for="{{ field.field_name }}" class="form-label">
|
||||
@@ -78,7 +78,7 @@
|
||||
<div class="form-actions">
|
||||
<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 %}>
|
||||
Start Scanning
|
||||
{% if edit_mode %}Save Changes{% else %}Start Scanning{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
<div class="count-location-container">
|
||||
<div class="location-header">
|
||||
<div class="location-info">
|
||||
<a href="{{ url_for('conssheets.index') }}" class="breadcrumb">← Back to Sessions</a>
|
||||
<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.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="header-values">
|
||||
{% for hv in header_values %}
|
||||
@@ -27,7 +33,7 @@
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div class="scan-form">
|
||||
@@ -109,15 +115,21 @@
|
||||
<i class="fa-solid fa-file-import"></i> Bulk Import Excel
|
||||
</button>
|
||||
</div>
|
||||
<div class="scans-grid scan-header-row" style="--field-count: {{ detail_fields|length }};">
|
||||
{% for field in detail_fields %}
|
||||
<div class="cs-scan-row-cell scan-col-header">{{ field.field_label }}</div>
|
||||
{% endfor %}
|
||||
<div class="cs-scan-row-cell scan-col-header">Status</div>
|
||||
</div>
|
||||
<div id="scansList" class="scans-grid" style="--field-count: {{ detail_fields|length }};">
|
||||
{% 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 }}"
|
||||
onclick="openScanDetail(this.dataset.detailId)">
|
||||
{% 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 %}
|
||||
<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
|
||||
{% 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 %}
|
||||
@@ -178,21 +190,7 @@
|
||||
</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 { 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>
|
||||
<!-- Styles moved to static/css/style.css -->
|
||||
|
||||
<script id="session-data" type="application/json">
|
||||
{
|
||||
@@ -247,6 +245,7 @@ function processSmartScan(barcode, confirm = false) {
|
||||
|
||||
// --- 1. HANDLE DUPLICATE CONFIRMATION ---
|
||||
if (data.needs_confirmation) {
|
||||
playErrorBeep();
|
||||
isSmartScan = true;
|
||||
currentDupKeyValue = barcode;
|
||||
if (data.duplicate_status === 'dup_same_session') {
|
||||
@@ -296,23 +295,26 @@ function processSmartScan(barcode, confirm = false) {
|
||||
}
|
||||
|
||||
// --- 3. STANDARD SUCCESS/FAIL ---
|
||||
if(smartInput) {
|
||||
smartInput.value = '';
|
||||
smartInput.focus();
|
||||
}
|
||||
feedbackArea.style.display = 'block';
|
||||
|
||||
if (data.success) {
|
||||
feedbackArea.style.background = 'rgba(40, 167, 69, 0.2)';
|
||||
feedbackArea.style.border = '1px solid #28a745';
|
||||
feedbackText.style.color = '#28a745';
|
||||
feedbackText.textContent = data.message;
|
||||
if (data.message.includes('Saved')) setTimeout(() => location.reload(), 800);
|
||||
if (data.detail_id && data.data) {
|
||||
addScanToList(data.detail_id, data.data, data.data.duplicate_status);
|
||||
}
|
||||
feedbackArea.style.display = 'none';
|
||||
if(smartInput) {
|
||||
smartInput.value = '';
|
||||
smartInput.focus();
|
||||
}
|
||||
} else {
|
||||
playErrorBeep();
|
||||
feedbackArea.style.display = 'block';
|
||||
feedbackArea.style.background = 'rgba(220, 53, 69, 0.2)';
|
||||
feedbackArea.style.border = '1px solid #dc3545';
|
||||
feedbackText.style.color = '#dc3545';
|
||||
feedbackText.textContent = data.message;
|
||||
if(smartInput) {
|
||||
smartInput.value = '';
|
||||
smartInput.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -320,7 +322,7 @@ function processSmartScan(barcode, confirm = false) {
|
||||
console.error(err); // Log it, don't popup alert to annoy user
|
||||
});
|
||||
}
|
||||
// Function to handle the "Save" click from the Details Modal
|
||||
|
||||
// Function to handle the "Save" click from the Details Modal
|
||||
function saveSmartScanData() {
|
||||
// 1. Validate we have the original barcode
|
||||
@@ -357,8 +359,12 @@ function saveSmartScanData() {
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('fieldsModal').style.display = 'none';
|
||||
// Optional: Small delay to let the user see it worked
|
||||
setTimeout(() => location.reload(), 300);
|
||||
if (data.detail_id && data.data) {
|
||||
addScanToList(data.detail_id, data.data, data.data.duplicate_status);
|
||||
}
|
||||
currentDupKeyValue = '';
|
||||
smartInput.value = '';
|
||||
smartInput.focus();
|
||||
} else if (data.needs_input) {
|
||||
alert("Error: Please fill in all required fields.");
|
||||
} else {
|
||||
@@ -372,6 +378,25 @@ function resetSmartScan() {
|
||||
document.getElementById('smartScanner').focus();
|
||||
document.getElementById('routerFeedback').style.display = 'none';
|
||||
}
|
||||
// --- ERROR BEEP ---
|
||||
function playErrorBeep() {
|
||||
try {
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gainNode = ctx.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
oscillator.type = 'square';
|
||||
oscillator.frequency.setValueAtTime(520, ctx.currentTime);
|
||||
gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + 0.4);
|
||||
} catch(e) {
|
||||
console.warn('Audio not available:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Standard variables
|
||||
let currentDupKeyValue = '';
|
||||
let currentDuplicateStatus = '';
|
||||
@@ -499,8 +524,8 @@ function submitScan() {
|
||||
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> Dup';
|
||||
row.className = 'cs-scan-row cs-scan-row-dup_same_session';
|
||||
row.querySelector('.cs-scan-row-status').innerHTML = '<span class="status-dot status-dot-blue"></span> Dup';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -525,16 +550,16 @@ function addScanToList(detailId, fieldValues, duplicateStatus) {
|
||||
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.className = 'cs-scan-row cs-scan-row-' + statusClass;
|
||||
scanRow.setAttribute('data-detail-id', detailId);
|
||||
scanRow.onclick = function() { openScanDetail(detailId); };
|
||||
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="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;
|
||||
scansList.insertBefore(scanRow, scansList.firstChild);
|
||||
const countSpan = document.getElementById('scanListCount');
|
||||
|
||||
@@ -30,26 +30,70 @@
|
||||
<!-- Active Sessions -->
|
||||
{% if sessions %}
|
||||
<div class="section-card">
|
||||
<h2 class="section-title">📋 My Active Sessions</h2>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-md);">
|
||||
<h2 class="section-title">📋 My Sessions</h2>
|
||||
<label class="checkbox-toggle">
|
||||
<input type="checkbox" id="showArchivedToggle" {% if show_archived %}checked{% endif %} onchange="toggleArchived(this.checked)">
|
||||
<span>Show archived/deleted</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sessions-list">
|
||||
{% for s in sessions %}
|
||||
<div class="session-list-item-container">
|
||||
<div class="session-list-item-container session-status-{{ s.status }}">
|
||||
<a href="{{ url_for('conssheets.scan_session', session_id=s.id) }}" class="session-list-item">
|
||||
<div class="session-list-info">
|
||||
<h3 class="session-list-name">{{ s.process_name }}</h3>
|
||||
<h3 class="session-list-name">{{ s.session_title }}</h3>
|
||||
<div class="session-list-meta">
|
||||
<span>Started: {{ s.created_at[:16] }}</span>
|
||||
<span>•</span>
|
||||
<span>{{ s.scan_count or 0 }} lots scanned</span>
|
||||
<span>{{ s.scan_count or 0 }} Scanned</span>
|
||||
</div>
|
||||
{% if s.header_preview %}
|
||||
<div class="session-list-meta" style="margin-top: var(--space-xs); font-size: 0.8rem;">
|
||||
{% for hf in s.header_preview %}
|
||||
<span><strong>{{ hf.field_label }}:</strong> {{ hf.field_value or '—' }}</span>
|
||||
{% if not loop.last %}<span>•</span>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if s.status == 'archived' %}
|
||||
<div class="session-list-meta" style="margin-top: var(--space-xs);">
|
||||
<span style="color: var(--color-warning);">📦 Archived</span>
|
||||
</div>
|
||||
{% elif s.status == 'deleted' %}
|
||||
<div class="session-list-meta" style="margin-top: var(--space-xs);">
|
||||
<span style="color: var(--color-danger);">🗑️ Deleted</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="session-list-action">
|
||||
<span class="arrow-icon">→</span>
|
||||
</div>
|
||||
</a>
|
||||
<button class="btn-archive" onclick="archiveSession({{ s.id }}, '{{ s.process_name }}')" title="Archive this session">
|
||||
🗑️
|
||||
</button>
|
||||
|
||||
{% if s.status == 'active' %}
|
||||
<button class="btn-session-action btn-archive" onclick="archiveSession({{ s.id }}, '{{ s.process_name }}')" title="Archive this session">
|
||||
📦
|
||||
</button>
|
||||
{% if session.role in ['owner', 'admin'] %}
|
||||
<button class="btn-session-action btn-delete" onclick="deleteSession({{ s.id }}, '{{ s.process_name }}')" title="Delete this session">
|
||||
❌
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif s.status == 'archived' %}
|
||||
<button class="btn-session-action btn-unarchive" onclick="unarchiveSession({{ s.id }}, '{{ s.process_name }}')" title="Restore to active">
|
||||
↩️
|
||||
</button>
|
||||
{% if session.role in ['owner', 'admin'] %}
|
||||
<button class="btn-session-action btn-delete" onclick="deleteSession({{ s.id }}, '{{ s.process_name }}')" title="Delete this session">
|
||||
❌
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif s.status == 'deleted' and session.role in ['owner', 'admin'] %}
|
||||
<button class="btn-session-action btn-restore" onclick="restoreSession({{ s.id }}, '{{ s.process_name }}')" title="Restore from deleted">
|
||||
🔄
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -63,89 +107,20 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.new-session-section {
|
||||
background: var(--color-surface);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.process-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-md);
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
margin-top: var(--space-xl);
|
||||
}
|
||||
|
||||
.session-list-item-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.session-list-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--color-surface);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-lg);
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.session-list-item:hover {
|
||||
border-color: var(--color-primary);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.session-list-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 var(--space-xs) 0;
|
||||
}
|
||||
|
||||
.session-list-meta {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-archive {
|
||||
background: var(--color-surface);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md);
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.btn-archive:hover {
|
||||
border-color: var(--color-danger);
|
||||
background: rgba(255, 51, 102, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Styles moved to static/css/style.css for maintainability -->
|
||||
<script>
|
||||
function toggleArchived(checked) {
|
||||
const url = new URL(window.location.href);
|
||||
if (checked) {
|
||||
url.searchParams.set('show_archived', '1');
|
||||
} else {
|
||||
url.searchParams.delete('show_archived');
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function archiveSession(sessionId, processName) {
|
||||
if (!confirm(`Archive this ${processName} session?\n\nYou can still view it later from the admin panel.`)) {
|
||||
if (!confirm(`Archive this ${processName} session?\n\nYou can restore it later.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,5 +137,62 @@ function archiveSession(sessionId, processName) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function unarchiveSession(sessionId, processName) {
|
||||
if (!confirm(`Restore ${processName} session to active?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/conssheets/session/${sessionId}/unarchive`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Error restoring session');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSession(sessionId, processName) {
|
||||
if (!confirm(`⚠️ DELETE ${processName} session?\n\nThis will mark all scanned data as deleted.\nThis action cannot be undone easily.\n\nAre you absolutely sure?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/conssheets/session/${sessionId}/delete`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Error deleting session');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreSession(sessionId, processName) {
|
||||
if (!confirm(`Restore ${processName} from deleted?\n\nThis will reactivate the session and all its data.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/conssheets/session/${sessionId}/restore`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Error restoring session');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -707,6 +707,138 @@ body {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Session List - Archive/Delete Actions */
|
||||
|
||||
.new-session-section {
|
||||
background: var(--color-surface);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.process-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-md);
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
margin-top: var(--space-xl);
|
||||
}
|
||||
|
||||
.session-list-item-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0; /* Remove gap */
|
||||
margin-bottom: var(--space-sm);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.session-list-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: transparent; /* Remove duplicate background */
|
||||
border: none; /* Remove border since container has it */
|
||||
border-radius: 0;
|
||||
padding: var(--space-lg);
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.session-list-item:hover {
|
||||
border-color: var(--color-primary);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.session-list-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.session-list-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.session-list-meta {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.checkbox-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-toggle input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-session-action {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 2px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
padding: var(--space-xl);
|
||||
cursor: pointer;
|
||||
font-size: 2rem;
|
||||
transition: var(--transition);
|
||||
min-width: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-session-action.btn-archive:hover {
|
||||
border-color: var(--color-warning);
|
||||
background: rgba(255, 170, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-session-action.btn-delete:hover {
|
||||
border-color: var(--color-danger);
|
||||
background: rgba(255, 51, 102, 0.1);
|
||||
}
|
||||
|
||||
.btn-session-action.btn-unarchive:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-session-action.btn-restore:hover {
|
||||
border-color: var(--color-success);
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.session-status-archived {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.session-status-archived .session-list-item-container {
|
||||
border-color: var(--color-warning);
|
||||
background: rgba(255, 170, 0, 0.08);
|
||||
}
|
||||
|
||||
.session-status-deleted {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.session-status-deleted .session-list-item-container {
|
||||
border-color: var(--color-danger);
|
||||
background: rgba(255, 51, 102, 0.08);
|
||||
}
|
||||
|
||||
/* ==================== FORM CONTAINER ==================== */
|
||||
|
||||
.form-container {
|
||||
@@ -1113,6 +1245,23 @@ body {
|
||||
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 ==================== */
|
||||
|
||||
.count-location-container {
|
||||
@@ -1130,7 +1279,7 @@ body {
|
||||
}
|
||||
|
||||
.location-label {
|
||||
font-size: 0.875rem;
|
||||
font-size: 1.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-text-muted);
|
||||
@@ -1303,9 +1452,25 @@ body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scan-header-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--field-count), 1fr) auto;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.scan-col-header {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.scan-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr 1.5fr;
|
||||
grid-template-columns: repeat(var(--field-count), 1fr) 1fr;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
background: var(--color-bg);
|
||||
|
||||
Reference in New Issue
Block a user