287 lines
11 KiB
HTML
287 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Shifts — Bid Checker{% endblock %}
|
||
{% block content %}
|
||
<div class="page-header">
|
||
<h1 class="page-title">Shift Management</h1>
|
||
<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>
|
||
<tr><th>Name</th><th>Days</th><th>Time</th><th>Users</th><th>Sites</th><th>Active</th><th>Actions</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for s in shifts %}
|
||
<tr>
|
||
<td><strong>{{ s.name }}</strong><br><span class="text-muted text-sm">{{ s.note or '' }}</span></td>
|
||
<td>{{ s.days_of_week }}</td>
|
||
<td class="text-muted">{{ s.start_time }} – {{ s.end_time }}</td>
|
||
<td>{{ s.user_count }}</td>
|
||
<td>{{ s.website_count }}</td>
|
||
<td><span class="badge badge-{{ 'success' if s.is_active else 'neutral' }}">{{ 'Active' if s.is_active else 'Inactive' }}</span></td>
|
||
<td class="actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="loadEditShift({{ s.id }})">✎ Edit</button>
|
||
<form method="post" action="{{ url_for('admin_shifts.delete', shift_id=s.id) }}"
|
||
style="display:inline" onsubmit="return confirm('Deactivate shift {{ s.name }}?')">
|
||
<button class="btn btn-danger btn-sm" type="submit">✕</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="7" class="text-center text-muted">No shifts configured.</td></tr>
|
||
{% endfor %}
|
||
</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">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-header">
|
||
<h3>New Shift</h3>
|
||
<button class="modal-close" onclick="closeModal('modal-create-shift')">✕</button>
|
||
</div>
|
||
<form method="post" action="{{ url_for('admin_shifts.create') }}">
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label">Shift Name *</label>
|
||
<input class="form-control" name="name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Days of Week</label>
|
||
<div class="day-checkboxes">
|
||
{% for label, val in day_map %}
|
||
<label class="day-check">
|
||
<input type="checkbox" name="days_of_week[]" value="{{ val }}"> {{ label }}
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group flex-1">
|
||
<label class="form-label">Start Time</label>
|
||
<input class="form-control" type="time" name="start_time" value="08:00">
|
||
</div>
|
||
<div class="form-group flex-1">
|
||
<label class="form-label">End Time</label>
|
||
<input class="form-control" type="time" name="end_time" value="17:00">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Assign Users</label>
|
||
<div class="checkbox-list">
|
||
{% for u in all_users %}
|
||
<label><input type="checkbox" name="user_ids[]" value="{{ u.id }}"> {{ u.username }}</label>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Assign Websites</label>
|
||
{% set daily_sites = all_sites | selectattr('check_type', 'equalto', 'daily') | list %}
|
||
{% set weekly_sites = all_sites | selectattr('check_type', 'equalto', 'weekly') | list %}
|
||
{% set other_sites = all_sites | rejectattr('check_type', 'in', ['daily','weekly']) | list %}
|
||
<div class="checkbox-list">
|
||
{% if daily_sites %}
|
||
<div class="site-type-header">Daily</div>
|
||
{% for w in daily_sites %}
|
||
<label><input type="checkbox" name="website_ids[]" value="{{ w.id }}"> {{ w.name }}</label>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% if weekly_sites %}
|
||
<div class="site-type-header">Weekly</div>
|
||
{% for w in weekly_sites %}
|
||
<label><input type="checkbox" name="website_ids[]" value="{{ w.id }}"> {{ w.name }}</label>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% if other_sites %}
|
||
<div class="site-type-header">Other</div>
|
||
{% for w in other_sites %}
|
||
<label><input type="checkbox" name="website_ids[]" value="{{ w.id }}"> {{ w.name }}</label>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Note</label>
|
||
<textarea class="form-control" name="note" rows="2"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-create-shift')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Create Shift</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Shift Modal -->
|
||
<div class="modal-overlay" id="modal-edit-shift">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-header">
|
||
<h3>Edit Shift</h3>
|
||
<button class="modal-close" onclick="closeModal('modal-edit-shift')">✕</button>
|
||
</div>
|
||
<form method="post" id="edit-shift-form">
|
||
<div class="modal-body" id="edit-shift-body"><div class="text-center text-muted">Loading…</div></div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-edit-shift')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
const ALL_USERS = {{ all_users | tojson }};
|
||
const ALL_SITES = {{ all_sites | tojson }};
|
||
const DAY_MAP = {{ day_map | tojson }};
|
||
|
||
function loadEditShift(id) {
|
||
fetch('/admin/shifts/' + id + '/detail')
|
||
.then(r => r.json())
|
||
.then(s => {
|
||
document.getElementById('edit-shift-form').action = '/admin/shifts/' + id + '/edit';
|
||
document.getElementById('edit-shift-body').innerHTML = buildShiftForm(s);
|
||
openModal('modal-edit-shift');
|
||
});
|
||
}
|
||
|
||
function buildShiftForm(s) {
|
||
const days = DAY_MAP.map(([lbl, val]) => `
|
||
<label class="day-check">
|
||
<input type="checkbox" name="days_of_week[]" value="${val}"
|
||
${s.days_of_week.includes(val)?'checked':''}>${lbl}
|
||
</label>`).join('');
|
||
|
||
const users = ALL_USERS.map(u => `
|
||
<label><input type="checkbox" name="user_ids[]" value="${u.id}"
|
||
${s.user_ids.includes(u.id)?'checked':''}> ${u.username}</label>`).join('');
|
||
|
||
const siteGroups = [
|
||
{ label: 'Daily', items: ALL_SITES.filter(w => w.check_type === 'daily') },
|
||
{ label: 'Weekly', items: ALL_SITES.filter(w => w.check_type === 'weekly') },
|
||
{ label: 'Other', items: ALL_SITES.filter(w => w.check_type !== 'daily' && w.check_type !== 'weekly') },
|
||
];
|
||
const sites = siteGroups.filter(g => g.items.length).map(g =>
|
||
`<div class="site-type-header">${g.label}</div>` +
|
||
g.items.map(w =>
|
||
`<label><input type="checkbox" name="website_ids[]" value="${w.id}"
|
||
${s.website_ids.includes(w.id)?'checked':''}> ${esc(w.name)}</label>`
|
||
).join('')
|
||
).join('');
|
||
|
||
return `
|
||
<div class="form-group"><label class="form-label">Shift Name *</label>
|
||
<input class="form-control" name="name" value="${esc(s.name)}" required></div>
|
||
<div class="form-group"><label class="form-label">Days of Week</label>
|
||
<div class="day-checkboxes">${days}</div></div>
|
||
<div class="form-row">
|
||
<div class="form-group flex-1"><label class="form-label">Start Time</label>
|
||
<input class="form-control" type="time" name="start_time" value="${esc(s.start_time)}"></div>
|
||
<div class="form-group flex-1"><label class="form-label">End Time</label>
|
||
<input class="form-control" type="time" name="end_time" value="${esc(s.end_time)}"></div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">Active</label>
|
||
<select class="form-control" name="is_active">
|
||
<option value="1" ${s.is_active?'selected':''}>Yes</option>
|
||
<option value="0" ${!s.is_active?'selected':''}>No</option>
|
||
</select></div>
|
||
<div class="form-group"><label class="form-label">Assign Users</label>
|
||
<div class="checkbox-list">${users}</div></div>
|
||
<div class="form-group"><label class="form-label">Assign Websites</label>
|
||
<div class="checkbox-list">${sites}</div></div>
|
||
<div class="form-group"><label class="form-label">Note</label>
|
||
<textarea class="form-control" name="note" rows="2">${esc(s.note)}</textarea></div>`;
|
||
}
|
||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
||
|
||
// Highlight today's column in the calendar
|
||
(function() {
|
||
var jsDay = new Date().getDay(); // 0=Sun … 6=Sat
|
||
var mysqlDay = String(jsDay === 0 ? 1 : jsDay + 1); // MySQL DAYOFWEEK: 1=Sun, 2=Mon…
|
||
var colIdx = DAY_MAP.findIndex(function(d) { return d[1] === mysqlDay; });
|
||
if (colIdx === -1) return;
|
||
var calTable = document.querySelector('#tab-calendar table');
|
||
if (!calTable) return;
|
||
calTable.querySelectorAll('tr').forEach(function(tr) {
|
||
var cells = tr.querySelectorAll('th, td');
|
||
var cell = cells[colIdx + 1]; // +1 for the leading Shift name column
|
||
if (cell) cell.classList.add('cal-today');
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<style>
|
||
.site-type-header {
|
||
font-size: .7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .06em;
|
||
color: var(--text-muted);
|
||
padding: .55rem .1rem .2rem;
|
||
margin-top: .25rem;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
.site-type-header:first-child { border-top: none; margin-top: 0; padding-top: .1rem; }
|
||
.cal-today { background: #eff6ff !important; }
|
||
.cal-today th, thead .cal-today {
|
||
background: #dbeafe !important;
|
||
font-weight: 700;
|
||
color: #1d4ed8;
|
||
}
|
||
</style>
|
||
{% endblock %}
|