diff --git a/db/init.sql b/db/init.sql index b0922e9..9ec683e 100644 --- a/db/init.sql +++ b/db/init.sql @@ -48,6 +48,15 @@ CREATE TABLE IF NOT EXISTS students ( ALTER TABLE students ADD COLUMN IF NOT EXISTS photo_path VARCHAR(255); ALTER TABLE students ADD COLUMN IF NOT EXISTS profile JSONB; +CREATE TABLE IF NOT EXISTS student_photos ( + id SERIAL PRIMARY KEY, + student_id INT NOT NULL REFERENCES students(id) ON DELETE CASCADE, + photo_path VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_student_photos_student_id ON student_photos(student_id); + CREATE TABLE IF NOT EXISTS entries ( id SERIAL PRIMARY KEY, student_name VARCHAR(150) NOT NULL, diff --git a/db/migration.sql b/db/migration.sql index 5a31d58..6b6a3c5 100644 --- a/db/migration.sql +++ b/db/migration.sql @@ -240,3 +240,12 @@ ALTER TABLE modules ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT ALTER TABLE modules ADD COLUMN IF NOT EXISTS photo_path VARCHAR(255); ALTER TABLE entries ADD COLUMN IF NOT EXISTS module_id INT REFERENCES modules(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS idx_entries_module_id ON entries(module_id); + +CREATE TABLE IF NOT EXISTS student_photos ( + id SERIAL PRIMARY KEY, + student_id INT NOT NULL REFERENCES students(id) ON DELETE CASCADE, + photo_path VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_student_photos_student_id ON student_photos(student_id); diff --git a/public/admin.css b/public/admin.css index 4a55848..3bd1a31 100644 --- a/public/admin.css +++ b/public/admin.css @@ -196,6 +196,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b .edit-modal .actions button:hover{opacity:.85} .toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#16a34a;color:#fff;padding:12px 24px;border-radius:var(--radius);font-size:.9rem;opacity:0;transition:opacity .3s;pointer-events:none;z-index:200} .toast.show{opacity:1} +.pf-photos{display:flex;flex-wrap:wrap;gap:10px} +.pf-photo{position:relative;width:96px;height:96px;border-radius:10px;overflow:hidden;border:2px solid var(--border);cursor:pointer;flex:0 0 auto} +.pf-photo.main{border-color:var(--accent)} +.pf-photo img{width:100%;height:100%;object-fit:cover;display:block} +.pf-badge{position:absolute;left:4px;bottom:4px;background:var(--accent);color:#fff;font-size:.65rem;font-weight:600;padding:2px 6px;border-radius:6px} +.pf-del{position:absolute;top:4px;right:4px;width:20px;height:20px;border:none;border-radius:50%;background:rgba(0,0,0,.6);color:#fff;font-size:.7rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center} +.pf-del:hover{background:#ef4444} /* --- Фото группы (хронология) --- */ .group-cover{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:10px;display:block;background:var(--bg)} diff --git a/public/js/students.js b/public/js/students.js index 20423e5..22b6e94 100644 --- a/public/js/students.js +++ b/public/js/students.js @@ -183,10 +183,12 @@ async function exportStudentProfile(id) { } let profileDataId = null; +let profilePhotos = []; function openProfileData(id) { profileDataId = id; loadGroupOptions(); + loadStudentPhotos(id); fetch(`${API}/api/students/${id}/profile`, { headers: hdr() }) .then(r => (r.ok ? r.json() : Promise.reject(new Error('load')))) .then(data => fillProfileForm(data.profile, data.name, data.photo_path)) @@ -199,6 +201,94 @@ function closeProfileData() { profileDataId = null; } +async function loadStudentPhotos(id) { + try { + const res = await fetch(`${API}/api/students/${id}/photos`, { headers: hdr() }); + if (!res.ok) return; + const data = await res.json(); + profilePhotos = data.photos || []; + document.getElementById('pfPhoto').value = data.photo_path || ''; + renderStudentPhotos(); + } catch (err) { + profilePhotos = []; + } +} + +function renderStudentPhotos() { + const wrap = document.getElementById('pfPhotos'); + if (!wrap) return; + const main = document.getElementById('pfPhoto').value.trim(); + if (!profilePhotos.length) { + wrap.innerHTML = 'Пока нет загруженных фото'; + return; + } + wrap.innerHTML = profilePhotos.map(p => ` +