05/24 Enhance functionalities
This commit is contained in:
@@ -22,6 +22,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Missed shifts alert -->
|
||||
{% if missed %}
|
||||
<div class="card mt-3" style="border-color:var(--warning-border)">
|
||||
<div class="card-header" style="background:var(--warning-bg)">
|
||||
<span class="card-title" style="color:var(--warning)">⚠ Missed Shifts Today</span>
|
||||
<span class="text-muted text-sm">Users with 0 sites checked so far</span>
|
||||
</div>
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Shift</th><th>User</th><th>Sites Checked</th></tr></thead>
|
||||
<tbody>
|
||||
{% for m in missed %}
|
||||
<tr>
|
||||
<td>{{ m.shift_name }}</td>
|
||||
<td>{{ m.full_name }}</td>
|
||||
<td class="text-muted">{{ m.checked_sites }} / {{ m.total_sites }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Per-user completion table -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
|
||||
@@ -3,9 +3,17 @@
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Shift Management</h1>
|
||||
<button class="btn btn-primary" onclick="openModal('modal-create-shift')">+ New Shift</button>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<button class="btn btn-ghost btn-sm tab-btn active" data-tab="tab-list" data-group="shifts-tabs">☰ List</button>
|
||||
<button class="btn btn-ghost btn-sm tab-btn" data-tab="tab-calendar" data-group="shifts-tabs">📅 Calendar</button>
|
||||
<button class="btn btn-primary" onclick="openModal('modal-create-shift')">+ New Shift</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shifts-tabs">
|
||||
|
||||
<!-- List tab -->
|
||||
<div class="tab-panel active" id="tab-list">
|
||||
<div class="card">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
@@ -34,6 +42,49 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar tab -->
|
||||
<div class="tab-panel" id="tab-calendar">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Weekly Schedule</span></div>
|
||||
<div style="overflow-x:auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="min-width:160px">Shift</th>
|
||||
{% for label, _ in day_map %}
|
||||
<th style="text-align:center;min-width:64px">{{ label }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in shifts if s.is_active %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ s.name }}</strong>
|
||||
<div class="text-muted text-sm">{{ s.start_time|hhmm }}–{{ s.end_time|hhmm }}</div>
|
||||
</td>
|
||||
{% for _, day_val in day_map %}
|
||||
<td style="text-align:center">
|
||||
{% if day_val in s.days_of_week %}
|
||||
<span class="badge badge-success" title="{{ s.name }} runs on this day">✓</span>
|
||||
{% else %}
|
||||
<span class="text-muted" style="font-size:.9rem">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="text-center text-muted">No active shifts.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /shifts-tabs -->
|
||||
|
||||
<!-- Create Shift Modal -->
|
||||
<div class="modal-overlay" id="modal-create-shift">
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary btn-sm" onclick="loadBids()">↻ Refresh</button>
|
||||
{% if session['user']['role'] == 'admin' %}
|
||||
<form method="post" action="{{ url_for('bid_tracker.send_reminders') }}" style="display:inline">
|
||||
<button class="btn btn-secondary btn-sm" type="submit" title="Email a digest of bids due within 7 days to admins">📧 Remind</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ── Split pane ─────────────────────────────────────────── -->
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forgot Password — Website Checker</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body class="login-wrap">
|
||||
<div class="login-box">
|
||||
|
||||
<div class="login-logo">
|
||||
<span class="brand-icon">🌐</span>
|
||||
<h1>Website Checker</h1>
|
||||
<p>Password Reset</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, msg in messages %}
|
||||
<div class="alert alert-{{ cat }} mb-2">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" action="{{ url_for('auth.forgot_password') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email"
|
||||
autocomplete="email" autofocus required
|
||||
placeholder="your@email.com">
|
||||
</div>
|
||||
<button class="btn btn-primary w-full" type="submit">Send Reset Link</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align:center;margin-top:1rem;font-size:.875rem">
|
||||
<a href="{{ url_for('auth.login') }}" style="color:var(--accent)">Back to sign in</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -34,6 +34,10 @@
|
||||
<button class="btn btn-primary" type="submit">Sign In</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align:center;margin-top:1rem;font-size:.875rem">
|
||||
<a href="{{ url_for('auth.forgot_password') }}" style="color:var(--accent)">Forgot password?</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Set New Password — Website Checker</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body class="login-wrap">
|
||||
<div class="login-box">
|
||||
|
||||
<div class="login-logo">
|
||||
<span class="brand-icon">🌐</span>
|
||||
<h1>Website Checker</h1>
|
||||
<p>Set New Password</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for cat, msg in messages %}
|
||||
<div class="alert alert-{{ cat }} mb-2">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" action="{{ url_for('auth.reset_password', token=token) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-group">
|
||||
<label for="password">New Password</label>
|
||||
<input type="password" id="password" name="password"
|
||||
autofocus required minlength="8"
|
||||
placeholder="Minimum 8 characters">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password"
|
||||
required placeholder="Repeat new password">
|
||||
</div>
|
||||
<button class="btn btn-primary w-full" type="submit">Set New Password</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align:center;margin-top:1rem;font-size:.875rem">
|
||||
<a href="{{ url_for('auth.login') }}" style="color:var(--accent)">Back to sign in</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -381,25 +381,29 @@ function esc(str) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Health dots ───────────────────────────────────────────── */
|
||||
/* ── Health dots (server-side probe) ───────────────────────── */
|
||||
document.querySelectorAll('.site-card').forEach(function(card) {
|
||||
var id = card.dataset.id;
|
||||
var url = card.dataset.url;
|
||||
var dot = document.getElementById('health-' + id);
|
||||
if (!dot || !url) return;
|
||||
var img = new Image();
|
||||
var t0 = Date.now();
|
||||
img.onload = function() {
|
||||
var ms = Date.now() - t0;
|
||||
dot.style.color = ms > 3000 ? '#d97706' : '#16a34a';
|
||||
dot.title = ms > 3000 ? 'Slow (' + ms + 'ms)' : 'Reachable (' + ms + 'ms)';
|
||||
};
|
||||
img.onerror = function() {
|
||||
var ms = Date.now() - t0;
|
||||
dot.style.color = ms < 6000 ? '#d97706' : '#dc2626';
|
||||
dot.title = ms < 6000 ? 'Reachable (restricted)' : 'Unreachable';
|
||||
};
|
||||
img.src = 'https://www.google.com/s2/favicons?domain=' + encodeURIComponent(url) + '&t=' + Date.now();
|
||||
if (!dot) return;
|
||||
fetch('/dashboard/health/' + id, {credentials: 'same-origin'})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.status === 'ok') {
|
||||
dot.style.color = d.ms > 3000 ? '#d97706' : '#16a34a';
|
||||
dot.title = 'Reachable (' + d.ms + 'ms)';
|
||||
} else if (d.status === 'timeout') {
|
||||
dot.style.color = '#d97706';
|
||||
dot.title = 'Timeout (>6s)';
|
||||
} else {
|
||||
dot.style.color = '#dc2626';
|
||||
dot.title = 'Unreachable';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
dot.style.color = '#9ca3af';
|
||||
dot.title = 'Health check failed';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user