restyling
This commit is contained in:
@@ -1,74 +1,254 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1 class="h3">Company dashboard</h1>
|
||||
<h1 class="page-title">Welcome{% if current_user and current_user.email %}, {{ current_user.email }}{% endif %}!</h1>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<h2 class="h5">Playlists</h2>
|
||||
<form method="post" action="{{ url_for('company.create_playlist') }}" class="card card-body mb-3">
|
||||
<div class="input-group">
|
||||
<input class="form-control" name="name" placeholder="New playlist name" required />
|
||||
<button class="btn btn-success" type="submit">Add</button>
|
||||
<div class="col-12">
|
||||
<div class="card card-elevated">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h5 mb-0">Playlists</h2>
|
||||
<form method="post" action="{{ url_for('company.create_playlist') }}" class="d-flex gap-2">
|
||||
<input class="form-control" name="name" placeholder="New playlist name" required />
|
||||
<button class="btn btn-brand" type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th class="text-end">Items</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in playlists %}
|
||||
<tr>
|
||||
<td><strong>{{ p.name }}</strong></td>
|
||||
<td class="text-end">{{ p.items|length }}</td>
|
||||
<td class="text-end">
|
||||
<div class="d-inline-flex gap-2">
|
||||
<a class="btn btn-ink btn-sm" href="{{ url_for('company.playlist_detail', playlist_id=p.id) }}">Open</a>
|
||||
<form method="post" action="{{ url_for('company.delete_playlist', playlist_id=p.id) }}" onsubmit="return confirm('Delete playlist? This will remove all items and unassign it from displays.');">
|
||||
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-muted">No playlists yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="list-group">
|
||||
{% for p in playlists %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a class="text-decoration-none" href="{{ url_for('company.playlist_detail', playlist_id=p.id) }}">
|
||||
<strong>{{ p.name }}</strong> <span class="text-muted">({{ p.items|length }} items)</span>
|
||||
</a>
|
||||
<form method="post" action="{{ url_for('company.delete_playlist', playlist_id=p.id) }}" onsubmit="return confirm('Delete playlist? This will remove all items and unassign it from displays.');">
|
||||
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted">No playlists yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h2 class="h5">Displays</h2>
|
||||
<div class="list-group">
|
||||
{% for d in displays %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div><strong>{{ d.name }}</strong></div>
|
||||
{% if d.description %}
|
||||
<div class="text-muted">{{ d.description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="min-width: 220px;">
|
||||
<form method="post" action="{{ url_for('company.update_display', display_id=d.id) }}" class="d-flex flex-column gap-2">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
name="description"
|
||||
placeholder="Description (e.g. entrance, office)"
|
||||
value="{{ d.description or '' }}"
|
||||
maxlength="200"
|
||||
/>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" name="playlist_id">
|
||||
<option value="">(none)</option>
|
||||
{% for p in playlists %}
|
||||
<option value="{{ p.id }}" {% if d.assigned_playlist_id == p.id %}selected{% endif %}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" type="submit">Save</button>
|
||||
<div class="col-12 mt-4">
|
||||
<div class="card card-elevated">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Displays</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{% for d in displays %}
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<div class="card display-gallery-card h-100">
|
||||
<div class="display-preview">
|
||||
<iframe
|
||||
title="Preview — {{ d.name }}"
|
||||
src="{{ url_for('display.display_player', token=d.token) }}?preview=1"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
></iframe>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<div>
|
||||
<div class="fw-bold">{{ d.name }}</div>
|
||||
<div class="text-muted small js-display-desc" data-display-id="{{ d.id }}">
|
||||
{{ d.description or "—" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-2 mt-auto">
|
||||
<select
|
||||
class="form-select form-select-sm js-playlist-select"
|
||||
data-display-id="{{ d.id }}"
|
||||
aria-label="Playlist selection"
|
||||
>
|
||||
<option value="">(none)</option>
|
||||
{% for p in playlists %}
|
||||
<option value="{{ p.id }}" {% if d.assigned_playlist_id == p.id %}selected{% endif %}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ink btn-sm js-edit-desc"
|
||||
data-display-id="{{ d.id }}"
|
||||
data-display-name="{{ d.name }}"
|
||||
data-current-desc="{{ d.description or '' }}"
|
||||
>
|
||||
Edit description
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12">
|
||||
<div class="text-muted">No displays. Ask admin to add displays.</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted">No displays. Ask admin to add displays.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast notifications -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
|
||||
<div id="companyToast" class="toast" role="alert" aria-live="polite" aria-atomic="true">
|
||||
<div class="toast-body" id="companyToastBody">Saved</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit description modal -->
|
||||
<div class="modal fade" id="editDescModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editDescModalTitle">Edit description</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label" for="editDescInput">Description</label>
|
||||
<textarea class="form-control" id="editDescInput" maxlength="200" rows="3" placeholder="Optional description (max 200 chars)"></textarea>
|
||||
<div class="form-text"><span id="editDescCount">0</span>/200</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-brand" id="editDescSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
const toastEl = document.getElementById('companyToast');
|
||||
const toastBodyEl = document.getElementById('companyToastBody');
|
||||
const toast = toastEl ? new bootstrap.Toast(toastEl, { delay: 2200 }) : null;
|
||||
|
||||
function showToast(message, variant) {
|
||||
if (!toast || !toastEl || !toastBodyEl) return;
|
||||
toastEl.classList.remove('text-bg-success', 'text-bg-danger', 'text-bg-secondary');
|
||||
if (variant) toastEl.classList.add(variant);
|
||||
toastBodyEl.textContent = message;
|
||||
toast.show();
|
||||
}
|
||||
|
||||
async function postDisplayUpdate(displayId, payload) {
|
||||
const res = await fetch(`/company/displays/${displayId}` , {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok || !data || !data.ok) {
|
||||
const msg = (data && data.error) ? data.error : 'Save failed';
|
||||
throw new Error(msg);
|
||||
}
|
||||
return data.display;
|
||||
}
|
||||
|
||||
// Playlist auto-save
|
||||
document.querySelectorAll('.js-playlist-select').forEach((sel) => {
|
||||
sel.addEventListener('change', async () => {
|
||||
const displayId = sel.dataset.displayId;
|
||||
const playlistId = sel.value || null;
|
||||
sel.disabled = true;
|
||||
try {
|
||||
await postDisplayUpdate(displayId, { playlist_id: playlistId });
|
||||
showToast('Playlist saved', 'text-bg-success');
|
||||
} catch (e) {
|
||||
showToast(e && e.message ? e.message : 'Save failed', 'text-bg-danger');
|
||||
} finally {
|
||||
sel.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Description modal
|
||||
const modalEl = document.getElementById('editDescModal');
|
||||
const modal = modalEl ? new bootstrap.Modal(modalEl) : null;
|
||||
const titleEl = document.getElementById('editDescModalTitle');
|
||||
const inputEl = document.getElementById('editDescInput');
|
||||
const countEl = document.getElementById('editDescCount');
|
||||
const saveBtn = document.getElementById('editDescSaveBtn');
|
||||
|
||||
let activeDisplayId = null;
|
||||
|
||||
function updateCount() {
|
||||
if (!inputEl || !countEl) return;
|
||||
countEl.textContent = String((inputEl.value || '').length);
|
||||
}
|
||||
if (inputEl) inputEl.addEventListener('input', updateCount);
|
||||
|
||||
document.querySelectorAll('.js-edit-desc').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
activeDisplayId = btn.dataset.displayId;
|
||||
const displayName = btn.dataset.displayName || 'Display';
|
||||
const currentDesc = btn.dataset.currentDesc || '';
|
||||
if (titleEl) titleEl.textContent = `Edit description — ${displayName}`;
|
||||
if (inputEl) inputEl.value = currentDesc;
|
||||
updateCount();
|
||||
if (modal) modal.show();
|
||||
});
|
||||
});
|
||||
|
||||
async function saveDescription() {
|
||||
if (!activeDisplayId || !inputEl) return;
|
||||
const desc = (inputEl.value || '').trim();
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const updated = await postDisplayUpdate(activeDisplayId, { description: desc });
|
||||
// Update visible description
|
||||
const descEl = document.querySelector(`.js-display-desc[data-display-id="${activeDisplayId}"]`);
|
||||
if (descEl) descEl.textContent = updated.description ? updated.description : '—';
|
||||
// Update button's stored value
|
||||
const btn = document.querySelector(`.js-edit-desc[data-display-id="${activeDisplayId}"]`);
|
||||
if (btn) btn.dataset.currentDesc = updated.description || '';
|
||||
showToast('Description saved', 'text-bg-success');
|
||||
if (modal) modal.hide();
|
||||
} catch (e) {
|
||||
showToast(e && e.message ? e.message : 'Save failed', 'text-bg-danger');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', saveDescription);
|
||||
}
|
||||
if (modalEl) {
|
||||
modalEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
saveDescription();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user