add project file upload to student form, project_files table with backup/restore, download in journal, and crash-proof entries endpoint

This commit is contained in:
dev
2026-09-06 11:29:34 +03:00
parent c2c5f2ed42
commit c5fdf006f5
5 changed files with 175 additions and 14 deletions
+78
View File
@@ -21,6 +21,21 @@ h1{font-size:1.5rem;font-weight:600;margin-bottom:8px;letter-spacing:-.02em;text
.photo-btns button:hover{border-color:var(--accent)}
.photo-btns .cam{background:var(--accent);color:#fff;border-color:var(--accent)}
.photo-btns .cam:hover{opacity:.85}
.photo-btns .files-btn p{font-size:.8rem;font-weight:500;margin:0}
.files-btn{width:100%;border-radius:var(--radius);padding:12px 16px;font-size:.9rem;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--card);color:var(--text);transition:border .15s,background .15s}
.files-btn:hover{border-color:var(--accent)}
.files-card{width:100%;border:1px dashed var(--border);border-radius:var(--radius);padding:16px;display:flex;flex-direction:column;gap:12px;background:var(--bg)}
.files-card .files-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
.files-card label{font-size:.8rem;font-weight:600;color:var(--text);text-transform:none;letter-spacing:0}
.files-card .files-clear{background:none;border:none;color:#ef4444;font-size:.8rem;cursor:pointer;padding:0;display:none}
.files-card .files-clear.show{display:block}
.files-list{display:flex;flex-direction:column;gap:6px;max-height:180px;overflow-y:auto}
.files-list:empty{display:none}
.files-list .file-row{display:flex;align-items:center;gap:8px;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 10px;font-size:.8rem}
.files-list .file-row .f-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
.files-list .file-row .f-size{color:var(--muted);flex-shrink:0}
.files-list .file-row button{background:none;border:none;color:var(--muted);cursor:pointer;font-size:.85rem;padding:0 2px;flex-shrink:0}
.files-list .file-row button:hover{color:#ef4444}
.preview-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:8px}
.preview{max-width:100%;width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--radius);background:var(--card);display:block;border:1px dashed var(--border)}
.preview.has-photo{border-style:solid}
@@ -82,6 +97,15 @@ nav a:hover{color:var(--text)}
<button type="button" class="cam" onclick="openCam()">Камера</button>
</div>
</div>
<div class="files-card">
<div class="files-head">
<label>Файлы проекта</label>
<button type="button" class="files-clear" id="filesClearBtn" onclick="clearFiles()">Очистить</button>
</div>
<button type="button" class="files-btn" onclick="document.getElementById('filesInput').click()">Выбрать файлы</button>
<input type="file" id="filesInput" multiple hidden>
<div class="files-list" id="filesList"></div>
</div>
</div>
<div class="fields-col">
@@ -139,6 +163,58 @@ const camModal = document.getElementById('camModal');
let capturedBlob = null;
let stream = null;
const descInput = document.getElementById('descInput');
const filesInput = document.getElementById('filesInput');
const filesList = document.getElementById('filesList');
const filesClearBtn = document.getElementById('filesClearBtn');
let selectedFiles = [];
function humanSize(b) {
if (b < 1024) return b + ' Б';
if (b < 1048576) return (b / 1024).toFixed(1) + ' КБ';
return (b / 1048576).toFixed(1) + ' МБ';
}
function renderFiles() {
filesList.innerHTML = '';
selectedFiles.forEach((f, i) => {
const row = document.createElement('div');
row.className = 'file-row';
const name = document.createElement('span');
name.className = 'f-name';
name.textContent = f.name;
name.title = f.name;
const size = document.createElement('span');
size.className = 'f-size';
size.textContent = humanSize(f.size);
const del = document.createElement('button');
del.type = 'button';
del.textContent = '✕';
del.onclick = () => removeFile(i);
row.append(name, size, del);
filesList.appendChild(row);
});
filesClearBtn.classList.toggle('show', selectedFiles.length > 0);
}
function removeFile(i) {
selectedFiles.splice(i, 1);
renderFiles();
}
function clearFiles() {
selectedFiles = [];
filesInput.value = '';
renderFiles();
}
filesInput.addEventListener('change', () => {
const keep = 10 - selectedFiles.length;
if (keep <= 0) { alert('Можно прикрепить не более 10 файлов'); filesInput.value = ''; return; }
selectedFiles = selectedFiles.concat([...filesInput.files].slice(0, keep));
if ([...filesInput.files].length > keep) alert('Можно прикрепить не более 10 файлов');
filesInput.value = '';
renderFiles();
});
const params = new URLSearchParams(location.search);
let studentName = '';
@@ -234,6 +310,7 @@ form.addEventListener('submit', async (e) => {
const fd = new FormData();
if (capturedBlob) fd.append('photo', capturedBlob, 'photo.jpg');
selectedFiles.forEach(f => fd.append('files', f, f.name));
fd.append('student_name', studentName || document.getElementById('nameInput').value);
fd.append('group_id', groupInput.value);
fd.append('description', document.getElementById('descInput').value);
@@ -250,6 +327,7 @@ form.addEventListener('submit', async (e) => {
if (!res.ok) throw new Error(await res.text());
form.reset();
removePhoto();
clearFiles();
toast.textContent = 'Готово!';
toast.classList.remove('error');
toast.classList.add('show');