Compare commits
2 Commits
ca368cbbfb
...
f7e25c8d1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7e25c8d1f | ||
|
|
672591c736 |
46
app.py
46
app.py
@@ -36,7 +36,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
|
||||
|
||||
|
||||
# 1. Define the version
|
||||
APP_VERSION = '0.9.0'
|
||||
APP_VERSION = '0.10.0'
|
||||
|
||||
# 2. Inject it into all templates automatically
|
||||
@app.context_processor
|
||||
@@ -94,20 +94,38 @@ def dashboard():
|
||||
|
||||
if role in ['owner', 'admin']:
|
||||
# Admin dashboard
|
||||
active_sessions = query_db('''
|
||||
SELECT s.*, u.full_name as created_by_name,
|
||||
COUNT(DISTINCT lc.location_count_id) as total_locations,
|
||||
SUM(CASE WHEN lc.status = 'completed' THEN 1 ELSE 0 END) as completed_locations,
|
||||
SUM(CASE WHEN lc.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress_locations
|
||||
FROM CountSessions s
|
||||
LEFT JOIN Users u ON s.created_by = u.user_id
|
||||
LEFT JOIN LocationCounts lc ON s.session_id = lc.session_id
|
||||
WHERE s.status = 'active'
|
||||
GROUP BY s.session_id
|
||||
ORDER BY s.created_timestamp DESC
|
||||
''')
|
||||
show_archived = request.args.get('show_archived', '0') == '1'
|
||||
|
||||
return render_template('admin_dashboard.html', sessions=active_sessions)
|
||||
if show_archived:
|
||||
# Show all sessions (active and archived)
|
||||
sessions_list = query_db('''
|
||||
SELECT s.*, u.full_name as created_by_name,
|
||||
COUNT(DISTINCT lc.location_count_id) as total_locations,
|
||||
SUM(CASE WHEN lc.status = 'completed' THEN 1 ELSE 0 END) as completed_locations,
|
||||
SUM(CASE WHEN lc.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress_locations
|
||||
FROM CountSessions s
|
||||
LEFT JOIN Users u ON s.created_by = u.user_id
|
||||
LEFT JOIN LocationCounts lc ON s.session_id = lc.session_id
|
||||
WHERE s.status IN ('active', 'archived')
|
||||
GROUP BY s.session_id
|
||||
ORDER BY s.status ASC, s.created_timestamp DESC
|
||||
''')
|
||||
else:
|
||||
# Show only active sessions
|
||||
sessions_list = query_db('''
|
||||
SELECT s.*, u.full_name as created_by_name,
|
||||
COUNT(DISTINCT lc.location_count_id) as total_locations,
|
||||
SUM(CASE WHEN lc.status = 'completed' THEN 1 ELSE 0 END) as completed_locations,
|
||||
SUM(CASE WHEN lc.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress_locations
|
||||
FROM CountSessions s
|
||||
LEFT JOIN Users u ON s.created_by = u.user_id
|
||||
LEFT JOIN LocationCounts lc ON s.session_id = lc.session_id
|
||||
WHERE s.status = 'active'
|
||||
GROUP BY s.session_id
|
||||
ORDER BY s.created_timestamp DESC
|
||||
''')
|
||||
|
||||
return render_template('admin_dashboard.html', sessions=sessions_list, show_archived=show_archived)
|
||||
|
||||
else:
|
||||
# Staff dashboard
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,12 @@ from db import query_db, execute_db, get_db
|
||||
from utils import login_required
|
||||
|
||||
counting_bp = Blueprint('counting', __name__)
|
||||
def get_active_session(session_id):
|
||||
"""Get session if it exists and is not archived. Returns None if invalid."""
|
||||
sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True)
|
||||
if not sess or sess['status'] == 'archived':
|
||||
return None
|
||||
return sess
|
||||
|
||||
@counting_bp.route('/count/<int:session_id>')
|
||||
@login_required
|
||||
@@ -29,6 +35,10 @@ def my_counts(session_id):
|
||||
flash('Session not found', 'danger')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
if sess['status'] == 'archived':
|
||||
flash('This session has been archived', 'warning')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
# Get this user's active bins
|
||||
active_bins = query_db('''
|
||||
SELECT lc.*,
|
||||
@@ -65,6 +75,11 @@ def my_counts(session_id):
|
||||
@login_required
|
||||
def start_bin_count(session_id):
|
||||
"""Start counting a new bin"""
|
||||
sess = get_active_session(session_id)
|
||||
if not sess:
|
||||
flash('Session not found or archived', 'warning')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
location_name = request.form.get('location_name', '').strip().upper()
|
||||
|
||||
if not location_name:
|
||||
@@ -125,7 +140,10 @@ def complete_location(location_count_id):
|
||||
def count_location(session_id, location_count_id):
|
||||
"""Count lots in a specific location"""
|
||||
# Get session info to determine type (Cycle Count vs Physical)
|
||||
sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True)
|
||||
sess = get_active_session(session_id)
|
||||
if not sess:
|
||||
flash('Session not found or archived', 'warning')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
location = query_db('''
|
||||
SELECT * FROM LocationCounts
|
||||
@@ -181,6 +199,10 @@ def count_location(session_id, location_count_id):
|
||||
@login_required
|
||||
def scan_lot(session_id, location_count_id):
|
||||
"""Process a lot scan with duplicate detection"""
|
||||
sess = get_active_session(session_id)
|
||||
if not sess:
|
||||
return jsonify({'success': False, 'message': 'Session not found or archived'})
|
||||
|
||||
data = request.get_json()
|
||||
lot_number = data.get('lot_number', '').strip()
|
||||
weight = data.get('weight')
|
||||
@@ -586,6 +608,10 @@ def recalculate_duplicate_status(session_id, lot_number, current_location):
|
||||
@login_required
|
||||
def finish_location(session_id, location_count_id):
|
||||
"""Finish counting a location"""
|
||||
sess = get_active_session(session_id)
|
||||
if not sess:
|
||||
return jsonify({'success': False, 'message': 'Session not found or archived'})
|
||||
|
||||
# Get location info
|
||||
location = query_db('SELECT * FROM LocationCounts WHERE location_count_id = ?',
|
||||
[location_count_id], one=True)
|
||||
|
||||
@@ -209,3 +209,36 @@ def get_status_details(session_id, status):
|
||||
except Exception as e:
|
||||
print(f"Error in get_status_details: {str(e)}")
|
||||
return jsonify({'success': False, 'message': f'Error: {str(e)}'})
|
||||
|
||||
@sessions_bp.route('/session/<int:session_id>/archive', methods=['POST'])
|
||||
@role_required('owner', 'admin')
|
||||
def archive_session(session_id):
|
||||
"""Archive a count session"""
|
||||
sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True)
|
||||
|
||||
if not sess:
|
||||
return jsonify({'success': False, 'message': 'Session not found'})
|
||||
|
||||
if sess['status'] == 'archived':
|
||||
return jsonify({'success': False, 'message': 'Session is already archived'})
|
||||
|
||||
execute_db('UPDATE CountSessions SET status = ? WHERE session_id = ?', ['archived', session_id])
|
||||
|
||||
return jsonify({'success': True, 'message': 'Session archived successfully'})
|
||||
|
||||
|
||||
@sessions_bp.route('/session/<int:session_id>/activate', methods=['POST'])
|
||||
@role_required('owner', 'admin')
|
||||
def activate_session(session_id):
|
||||
"""Reactivate an archived session"""
|
||||
sess = query_db('SELECT * FROM CountSessions WHERE session_id = ?', [session_id], one=True)
|
||||
|
||||
if not sess:
|
||||
return jsonify({'success': False, 'message': 'Session not found'})
|
||||
|
||||
if sess['status'] != 'archived':
|
||||
return jsonify({'success': False, 'message': 'Session is not archived'})
|
||||
|
||||
execute_db('UPDATE CountSessions SET status = ? WHERE session_id = ?', ['active', session_id])
|
||||
|
||||
return jsonify({'success': True, 'message': 'Session activated successfully'})
|
||||
Binary file not shown.
@@ -2119,3 +2119,36 @@ body {
|
||||
.scroll-to-bottom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==================== ARCHIVED SESSIONS ==================== */
|
||||
|
||||
.session-archived {
|
||||
opacity: 0.7;
|
||||
border-color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
.session-archived:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.archived-badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.5rem;
|
||||
background: var(--color-text-dim);
|
||||
color: var(--color-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
margin-left: var(--space-sm);
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* Session Detail Header Actions */
|
||||
.session-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.session-actions-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -23,18 +23,34 @@
|
||||
</script>
|
||||
|
||||
<div class="dashboard-header">
|
||||
<h1 class="page-title">Admin Dashboard</h1>
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">Admin Dashboard</h1>
|
||||
<label class="filter-toggle">
|
||||
<input type="checkbox" id="showArchived" {% if show_archived %}checked{% endif %} onchange="toggleArchived()">
|
||||
<span class="filter-label">Show Archived</span>
|
||||
</label>
|
||||
</div>
|
||||
<a href="{{ url_for('sessions.create_session') }}" class="btn btn-primary">
|
||||
<span class="btn-icon">+</span> New Session
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleArchived() {
|
||||
const checked = document.getElementById('showArchived').checked;
|
||||
window.location.href = '{{ url_for("dashboard") }}' + (checked ? '?show_archived=1' : '');
|
||||
}
|
||||
</script>
|
||||
|
||||
{% if sessions %}
|
||||
<div class="sessions-grid">
|
||||
{% for session in sessions %}
|
||||
<div class="session-card">
|
||||
<div class="session-card {% if session.status == 'archived' %}session-archived{% endif %}">
|
||||
<div class="session-card-header">
|
||||
<h3 class="session-name">{{ session.session_name }}</h3>
|
||||
<h3 class="session-name">
|
||||
{{ session.session_name }}
|
||||
{% if session.status == 'archived' %}<span class="archived-badge">ARCHIVED</span>{% endif %}
|
||||
</h3>
|
||||
<span class="session-type-badge session-type-{{ session.session_type }}">
|
||||
{{ 'Full Physical' if session.session_type == 'full_physical' else 'Cycle Count' }}
|
||||
</span>
|
||||
|
||||
@@ -6,13 +6,22 @@
|
||||
<div class="session-detail-container">
|
||||
<div class="session-detail-header">
|
||||
<div>
|
||||
<a href="{{ url_for('dashboard') }}" class="breadcrumb">← Back to Dashboard</a>
|
||||
<h1 class="page-title">{{ count_session.session_name }}</h1>
|
||||
<!-- Fixed variable name from session.session_type to count_session.session_type -->
|
||||
<a href="{{ url_for('dashboard') }}{% if count_session.status == 'archived' %}?show_archived=1{% endif %}" class="breadcrumb">← Back to Dashboard</a>
|
||||
<h1 class="page-title">
|
||||
{{ count_session.session_name }}
|
||||
{% if count_session.status == 'archived' %}<span class="archived-badge">ARCHIVED</span>{% endif %}
|
||||
</h1>
|
||||
<span class="session-type-badge session-type-{{ count_session.session_type }}">
|
||||
{{ 'Full Physical' if count_session.session_type == 'full_physical' else 'Cycle Count' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="session-actions-header">
|
||||
{% if count_session.status == 'archived' %}
|
||||
<button class="btn btn-success" onclick="activateSession()">✓ Activate Session</button>
|
||||
{% else %}
|
||||
<button class="btn btn-secondary" onclick="archiveSession()">📦 Archive Session</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Baseline Upload Section -->
|
||||
@@ -675,5 +684,38 @@ document.addEventListener('keydown', function(e) {
|
||||
closeReopenConfirm();
|
||||
}
|
||||
});
|
||||
function archiveSession() {
|
||||
if (!confirm('Archive this session? It will be hidden from the main dashboard but can be reactivated later.')) return;
|
||||
|
||||
fetch('{{ url_for("sessions.archive_session", session_id=count_session.session_id) }}', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = '{{ url_for("dashboard") }}';
|
||||
} else {
|
||||
alert(data.message || 'Error archiving session');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function activateSession() {
|
||||
if (!confirm('Reactivate this session? It will appear on the main dashboard again.')) return;
|
||||
|
||||
fetch('{{ url_for("sessions.activate_session", session_id=count_session.session_id) }}', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Error activating session');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user