Files
web-petting/templates/admin/schedule_edit.html
T
Aleksandr Bogomiakov 79c287eaa0
Build and Publish / Build and Publish Docker Image (push) Successful in 1m42s
Reworked admin style
2026-08-09 03:19:47 +01:00

346 lines
15 KiB
HTML

{% extends "admin/layout.html" %}
{% let active_page = "schedule" %}
{% block title %}{{ t.schedule_edit_title }}{% endblock %}
{% block content %}
<div class="page-head">
<h1>{{ t.schedule_title }}</h1>
</div>
<section class="admin-section">
<header class="admin-section-head">
<span class="admin-section-icon" aria-hidden="true">📋</span>
<div>
<h2>{{ t.schedule_edit_title }}</h2>
<p>{{ client.name }}</p>
</div>
</header>
<div class="admin-section-body">
<form method="post" action="/admin/schedule/{{ visit.id }}/save">
<!-- Client -->
<div class="field">
<label class="label">{{ t.schedule_client }}</label>
<div class="control">
<input class="input" type="text" value="{{ client.name }}{% if let Some(p) = client.phone.as_deref() %} ({{ p }}){% endif %}" readonly>
</div>
</div>
<!-- Admin -->
<div class="field">
<label class="label">{{ t.schedule_admin }}</label>
<div class="control">
<div class="select is-fullwidth">
<select name="user_id">
{% for u in &users %}
<option value="{{ u.id }}" {% if u.id.unwrap() == visit.user_id.primary_key().unwrap() %}selected{% endif %}>
{{ u.display_name.as_deref().unwrap_or(&u.login) }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<!-- Date -->
<div class="field">
<label class="label">{{ t.schedule_date }}</label>
<div class="control">
<input class="input" type="date" name="visit_date" value="{{ visit.visit_date }}" required>
</div>
</div>
<!-- Time -->
<div class="field">
<label class="label">{{ t.schedule_default_time }}</label>
<div class="columns is-mobile time-columns">
<div class="column">
<div class="control">
<input class="input" type="time" name="time_start" value="{{ visit.time_start }}" required>
</div>
</div>
<div class="column">
<div class="control">
<input class="input" type="time" name="time_end" value="{{ visit.time_end }}" required>
</div>
</div>
</div>
</div>
<!-- Status -->
<div class="field">
<label class="label">{{ t.schedule_status }}</label>
<input type="hidden" name="status" id="statusInput" value="{{ visit.status }}">
<div class="status-picker">
<button type="button" class="status-btn status-btn-scheduled {% if visit.status == "scheduled" %}is-active{% endif %}" data-value="scheduled">
<span class="status-btn-icon">📅</span>{{ t.visit_status_scheduled }}
</button>
<button type="button" class="status-btn status-btn-completed {% if visit.status == "completed" %}is-active{% endif %}" data-value="completed">
<span class="status-btn-icon"></span>{{ t.visit_status_completed }}
</button>
<button type="button" class="status-btn status-btn-cancelled {% if visit.status == "cancelled" %}is-active{% endif %}" data-value="cancelled">
<span class="status-btn-icon"></span>{{ t.visit_status_cancelled }}
</button>
</div>
</div>
<!-- Private Notes -->
<div class="field">
<label class="label">{{ t.schedule_notes }}</label>
<div class="control">
<textarea class="textarea" name="notes" rows="2">{{ visit.notes.as_deref().unwrap_or("") }}</textarea>
</div>
</div>
<!-- Public Notes (visible to client) -->
<div class="field">
<label class="label">{{ t.schedule_public_notes }}</label>
<div class="control">
<textarea class="textarea" name="public_notes" rows="2">{{ visit.public_notes.as_deref().unwrap_or("") }}</textarea>
</div>
</div>
{% if let Some(fb) = visit.client_feedback.as_deref() %}
<div class="mb-4">
<label class="label">{{ t.schedule_client_feedback }}</label>
<div class="admin-note admin-note-info">{{ fb }}</div>
</div>
{% endif %}
<hr>
<!-- Media -->
<div class="section-inline-head">
<label class="label">{{ t.nav_media }}</label>
<button type="button" class="button is-info is-small is-outlined" onclick="document.getElementById('uploadModal').classList.add('is-open')">+ {{ t.media_upload }}</button>
</div>
{% if media.is_empty() %}
<p class="empty-state mb-4">{{ t.media_empty }}</p>
{% else %}
<div class="visit-media-grid">
{% for m in &media %}
<div class="visit-media-item">
{% if m.file_type == "photo" %}
<a href="{{ m.url }}" data-lightbox="photo">
<span class="photo-thumb media-loading-frame is-loading">
<img src="{{ m.thumbnail_url }}" alt="" loading="lazy" data-media-load>
</span>
</a>
{% else %}
<a href="{{ m.url }}" data-lightbox="video">
<div class="video-thumb-sm media-loading-frame{% if !m.thumbnail_url.is_empty() %} is-loading{% endif %}" data-video-preview>
{% if !m.thumbnail_url.is_empty() %}
<img class="video-preview-sprite" src="{{ m.thumbnail_url }}" alt="" loading="lazy" data-media-load data-preview-frames="4">
{% endif %}
<span class="video-play"></span>
</div>
</a>
{% endif %}
{% if let Some(cap) = m.caption.as_deref() %}
<div class="media-cap">{{ cap }}</div>
{% endif %}
<div class="visit-media-delete">
<button type="submit" form="visit-media-delete-{{ m.id.unwrap() }}" class="button is-small is-danger is-outlined">{{ t.media_delete }}</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<hr>
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
</form>
{% for m in &media %}
<form id="visit-media-delete-{{ m.id.unwrap() }}" method="post" action="/admin/media/{{ m.id.unwrap() }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');"></form>
{% endfor %}
<hr>
<form method="post" action="/admin/schedule/{{ visit.id }}/delete" onsubmit="return confirm('{{ t.schedule_delete_confirm }}');">
<button type="submit" class="button is-danger is-outlined is-fullwidth is-small">{{ t.schedule_delete }}</button>
</form>
</div>
</section>
<!-- Upload Modal -->
<div class="upload-modal-bg" id="uploadModal">
<div class="upload-modal">
<div class="modal-head">
<h3>{{ t.media_upload_title }}</h3>
<button type="button" id="uploadModalClose" class="modal-close"></button>
</div>
<form id="uploadForm" action="/admin/media/{{ visit.id }}/upload/submit" enctype="multipart/form-data">
<div class="field">
<label class="label">{{ t.media_choose_files }}</label>
<div class="control">
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
</div>
<p id="fileCount" class="field-help"></p>
<div id="uploadQueue" class="upload-queue"></div>
</div>
<div class="field">
<label class="label">{{ t.media_caption }}</label>
<div class="control">
<input class="input" type="text" name="caption" placeholder="{{ t.media_caption }}">
</div>
</div>
<!-- Progress -->
<div id="uploadProgress" class="upload-progress">
<div class="upload-progress-head">
<span id="uploadStatusText">{{ t.media_upload }}...</span>
<span id="uploadPercent">0%</span>
</div>
<div class="upload-progress-track">
<div id="uploadBar" class="upload-progress-bar"></div>
</div>
</div>
<button type="submit" id="uploadSubmit" class="button is-primary is-fullwidth">{{ t.media_upload }}</button>
</form>
</div>
</div>
<script>
// Status picker
document.querySelectorAll('.status-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('.status-btn').forEach(function(b) { b.classList.remove('is-active'); });
btn.classList.add('is-active');
document.getElementById('statusInput').value = btn.dataset.value;
});
});
</script>
<script>
(function() {
var modal = document.getElementById('uploadModal');
var form = document.getElementById('uploadForm');
var filesInput = document.getElementById('uploadFiles');
var fileCount = document.getElementById('fileCount');
var queue = document.getElementById('uploadQueue');
var progress = document.getElementById('uploadProgress');
var bar = document.getElementById('uploadBar');
var percent = document.getElementById('uploadPercent');
var statusText = document.getElementById('uploadStatusText');
var submitBtn = document.getElementById('uploadSubmit');
// Close modal on backdrop click
modal.addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
document.getElementById('uploadModalClose').addEventListener('click', closeModal);
function closeModal() {
if (submitBtn.disabled) return; // prevent close during upload
modal.classList.remove('is-open');
}
// Show selected file count
filesInput.addEventListener('change', function() {
var n = this.files.length;
fileCount.textContent = n > 0 ? ('{{ t.media_files_selected }}: ' + n) : '';
queue.replaceChildren();
Array.from(this.files).forEach(function(file) {
var item = document.createElement('div');
item.className = 'upload-queue-item';
var icon = document.createElement('span');
icon.className = 'upload-queue-icon';
icon.textContent = file.type.indexOf('video/') === 0 ? '🎬' : '🖼️';
var details = document.createElement('div');
var name = document.createElement('div');
name.className = 'upload-queue-name';
name.textContent = file.name;
var state = document.createElement('div');
state.className = 'upload-queue-state';
state.textContent = '0%';
var track = document.createElement('div');
track.className = 'upload-queue-track';
var itemBar = document.createElement('div');
itemBar.className = 'upload-queue-bar';
track.appendChild(itemBar);
details.append(name, state, track);
item.append(icon, details);
queue.appendChild(item);
});
});
function updateQueue(state, progressValue, processing) {
queue.querySelectorAll('.upload-queue-item').forEach(function(item) {
item.querySelector('.upload-queue-state').textContent = state;
var itemBar = item.querySelector('.upload-queue-bar');
itemBar.classList.toggle('is-processing', processing);
if (!processing) itemBar.style.width = progressValue + '%';
});
}
// Submit via XHR for progress tracking
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!filesInput.files.length) return;
var data = new FormData(form);
var xhr = new XMLHttpRequest();
// Show progress bar, disable submit
progress.style.display = 'block';
submitBtn.disabled = true;
submitBtn.textContent = '{{ t.media_upload_sending }}';
statusText.textContent = '{{ t.media_upload_sending }}';
bar.classList.remove('is-processing');
bar.style.width = '0%';
percent.textContent = '0%';
xhr.upload.addEventListener('progress', function(ev) {
if (!ev.lengthComputable) return;
var pct = Math.round(ev.loaded / ev.total * 100);
bar.style.width = pct + '%';
percent.textContent = pct + '%';
updateQueue(pct + '%', pct, false);
if (pct === 100) {
statusText.textContent = '{{ t.media_upload_processing }}';
percent.textContent = '•••';
bar.classList.add('is-processing');
updateQueue('{{ t.media_upload_processing }}', 100, true);
}
});
xhr.upload.addEventListener('load', function() {
statusText.textContent = '{{ t.media_upload_processing }}';
percent.textContent = '•••';
bar.classList.add('is-processing');
updateQueue('{{ t.media_upload_processing }}', 100, true);
});
xhr.addEventListener('load', function() {
if (xhr.status >= 200 && xhr.status < 400) {
bar.style.width = '100%';
bar.classList.remove('is-processing');
percent.textContent = '100%';
statusText.textContent = '{{ t.media_upload_done }}';
updateQueue('{{ t.media_upload_done }}', 100, false);
// Reload page to show uploaded media
setTimeout(function() { window.location.reload(); }, 300);
} else {
bar.classList.remove('is-processing');
statusText.textContent = 'Ошибка загрузки (' + xhr.status + ')';
updateQueue('Ошибка загрузки', 0, false);
submitBtn.disabled = false;
submitBtn.textContent = '{{ t.media_upload }}';
}
});
xhr.addEventListener('error', function() {
bar.classList.remove('is-processing');
statusText.textContent = '{{ t.media_upload_connection_error }}';
updateQueue('{{ t.media_upload_connection_error }}', 0, false);
submitBtn.disabled = false;
submitBtn.textContent = '{{ t.media_upload }}';
});
xhr.open('POST', form.action);
xhr.send(data);
});
})();
</script>
{% endblock %}