2026-04-29 17:49:07 +03:00
|
|
|
{% extends "admin/layout.html" %}
|
|
|
|
|
{% let active_page = "media" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}{{ t.media_upload_title }}{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<div class="page-head">
|
|
|
|
|
<h1>{{ t.media_upload_title }}</h1>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-card">
|
|
|
|
|
<div style="margin-bottom:1rem;font-size:0.9rem;color:#666;">
|
|
|
|
|
<div><strong>{{ t.schedule_client }}:</strong> {{ client_name }}</div>
|
|
|
|
|
<div><strong>{{ t.schedule_date }}:</strong> {{ visit_label }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-18 14:47:04 +03:00
|
|
|
<form id="uploadForm" action="/admin/media/{{ visit_id }}/upload/submit" enctype="multipart/form-data">
|
2026-04-29 17:49:07 +03:00
|
|
|
<div class="field">
|
|
|
|
|
<label class="label">{{ t.media_choose_files }}</label>
|
|
|
|
|
<div class="control">
|
2026-05-18 14:47:04 +03:00
|
|
|
<input class="input" type="file" id="uploadFiles" name="files" multiple accept="image/*,video/*" required>
|
2026-04-29 17:49:07 +03:00
|
|
|
</div>
|
2026-05-18 14:47:04 +03:00
|
|
|
<p id="fileCount" style="font-size:0.8rem;color:#888;margin-top:0.3rem;"></p>
|
2026-08-09 01:05:47 +01:00
|
|
|
<div id="uploadQueue" class="upload-queue"></div>
|
2026-04-29 17:49:07 +03:00
|
|
|
</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>
|
|
|
|
|
|
2026-05-18 14:47:04 +03:00
|
|
|
<!-- Progress -->
|
|
|
|
|
<div id="uploadProgress" style="display:none;margin-bottom:1rem;">
|
|
|
|
|
<div style="display:flex;justify-content:space-between;font-size:0.82rem;color:#555;margin-bottom:0.3rem;">
|
|
|
|
|
<span id="uploadStatusText">{{ t.media_upload }}...</span>
|
|
|
|
|
<span id="uploadPercent">0%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="background:#e8e8e8;border-radius:99px;height:8px;overflow:hidden;">
|
|
|
|
|
<div id="uploadBar" style="height:100%;width:0%;background:linear-gradient(90deg,#6c63ff,#b06cff);border-radius:99px;transition:width 0.2s;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button type="submit" id="uploadSubmit" class="button is-primary is-fullwidth">{{ t.media_upload }}</button>
|
2026-04-29 17:49:07 +03:00
|
|
|
</form>
|
2026-05-18 14:47:04 +03:00
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
var form = document.getElementById('uploadForm');
|
|
|
|
|
var filesInput = document.getElementById('uploadFiles');
|
|
|
|
|
var fileCount = document.getElementById('fileCount');
|
2026-08-09 01:05:47 +01:00
|
|
|
var queue = document.getElementById('uploadQueue');
|
2026-05-18 14:47:04 +03:00
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
filesInput.addEventListener('change', function() {
|
|
|
|
|
var n = this.files.length;
|
2026-08-09 01:05:47 +01:00
|
|
|
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);
|
|
|
|
|
});
|
2026-05-18 14:47:04 +03:00
|
|
|
});
|
|
|
|
|
|
2026-08-09 01:05:47 +01:00
|
|
|
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 + '%';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 14:47:04 +03:00
|
|
|
form.addEventListener('submit', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!filesInput.files.length) return;
|
|
|
|
|
|
|
|
|
|
var data = new FormData(form);
|
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
|
|
|
|
|
|
progress.style.display = 'block';
|
|
|
|
|
submitBtn.disabled = true;
|
2026-08-09 01:05:47 +01:00
|
|
|
submitBtn.textContent = '{{ t.media_upload_sending }}';
|
|
|
|
|
statusText.textContent = '{{ t.media_upload_sending }}';
|
|
|
|
|
bar.classList.remove('is-processing');
|
2026-05-18 14:47:04 +03:00
|
|
|
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 + '%';
|
2026-08-09 01:05:47 +01:00
|
|
|
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);
|
2026-05-18 14:47:04 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('load', function() {
|
|
|
|
|
if (xhr.status >= 200 && xhr.status < 400) {
|
|
|
|
|
bar.style.width = '100%';
|
2026-08-09 01:05:47 +01:00
|
|
|
bar.classList.remove('is-processing');
|
2026-05-18 14:47:04 +03:00
|
|
|
percent.textContent = '100%';
|
2026-08-09 01:05:47 +01:00
|
|
|
statusText.textContent = '{{ t.media_upload_done }}';
|
|
|
|
|
updateQueue('{{ t.media_upload_done }}', 100, false);
|
2026-05-18 14:47:04 +03:00
|
|
|
setTimeout(function() { window.location.href = xhr.responseURL || '/admin/media'; }, 300);
|
|
|
|
|
} else {
|
2026-08-09 01:05:47 +01:00
|
|
|
bar.classList.remove('is-processing');
|
2026-05-18 14:47:04 +03:00
|
|
|
statusText.textContent = 'Ошибка загрузки (' + xhr.status + ')';
|
2026-08-09 01:05:47 +01:00
|
|
|
updateQueue('Ошибка загрузки', 0, false);
|
2026-05-18 14:47:04 +03:00
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
submitBtn.textContent = '{{ t.media_upload }}';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('error', function() {
|
2026-08-09 01:05:47 +01:00
|
|
|
bar.classList.remove('is-processing');
|
|
|
|
|
statusText.textContent = '{{ t.media_upload_connection_error }}';
|
|
|
|
|
updateQueue('{{ t.media_upload_connection_error }}', 0, false);
|
2026-05-18 14:47:04 +03:00
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
submitBtn.textContent = '{{ t.media_upload }}';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.open('POST', form.action);
|
|
|
|
|
xhr.send(data);
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
2026-04-29 17:49:07 +03:00
|
|
|
</div>
|
2026-08-09 01:05:47 +01:00
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.upload-queue { display:flex; flex-direction:column; gap:.4rem; margin-top:.65rem; }
|
|
|
|
|
.upload-queue:empty { display:none; }
|
|
|
|
|
.upload-queue-item { display:grid; grid-template-columns:34px minmax(0,1fr); gap:.55rem; align-items:center; padding:.45rem .55rem; background:#f7f6ff; border-radius:8px; }
|
|
|
|
|
.upload-queue-icon { width:34px; height:34px; display:flex; align-items:center; justify-content:center; border-radius:7px; background:#ebe8ff; font-size:1.05rem; }
|
|
|
|
|
.upload-queue-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.78rem; color:#494467; }
|
|
|
|
|
.upload-queue-state { font-size:.68rem; color:#8a84a5; }
|
|
|
|
|
.upload-queue-track { height:3px; overflow:hidden; border-radius:99px; background:#dedbea; margin-top:.25rem; }
|
|
|
|
|
.upload-queue-bar { height:100%; width:0; background:#7567e8; transition:width .2s; }
|
|
|
|
|
#uploadBar.is-processing, .upload-queue-bar.is-processing { width:35% !important; animation:upload-processing 1.15s ease-in-out infinite; }
|
|
|
|
|
@keyframes upload-processing { from { transform:translateX(-110%); } to { transform:translateX(300%); } }
|
|
|
|
|
</style>
|
2026-04-29 17:49:07 +03:00
|
|
|
{% endblock %}
|