diff --git a/index.html b/index.html index adc319f..c9316b5 100644 --- a/index.html +++ b/index.html @@ -122,7 +122,6 @@ Артист Артист (Z) - Организатор (Rom) Постановщик (Rop) Техник (Rot) Комиссионер (Rok) @@ -132,7 +131,7 @@ - Нет добавленных артистов + Нет добавленных артистов diff --git a/js/app.js b/js/app.js index c7834ce..42bfe81 100644 --- a/js/app.js +++ b/js/app.js @@ -31,9 +31,12 @@ s: document.getElementById('summary-s'), }; - let artists = []; // [{ name, roles: { artist, organizer, director, technician, commissioner } }] + let artists = []; // [{ name, roles: { artist, director, technician, commissioner } }] let allArtistsList = []; + // Track which fields were auto-calculated so we can recalc them when roster changes + const autoCalculated = new Set(); + async function loadArtists() { try { allArtistsList = window.__ARTISTS__ || []; @@ -68,10 +71,9 @@ } function countRoles() { - const counts = { artist: 0, organizer: 0, director: 0, technician: 0, commissioner: 0 }; + const counts = { artist: 0, director: 0, technician: 0, commissioner: 0 }; for (const a of artists) { if (a.roles.artist) counts.artist++; - if (a.roles.organizer) counts.organizer++; if (a.roles.director) counts.director++; if (a.roles.technician) counts.technician++; if (a.roles.commissioner) counts.commissioner++; @@ -79,11 +81,16 @@ return counts; } - function calculate() { + function getMaterials(vals) { + return (vals.pz || 0) + (vals.tz || 0) + (vals.lz || 0) * (vals.nl || 0); + } + + function calculate(opts = {}) { + const { fromRosterChange } = opts; messageEl.textContent = ''; messageEl.className = 'input-info mt-4'; - const vals = { + let vals = { s: getValue(inputs.s), pz: getValue(inputs.pz), tz: getValue(inputs.tz), @@ -96,10 +103,10 @@ const filled = editableKeys.filter(k => vals[k] !== null); const missing = editableKeys.filter(k => vals[k] === null); - // Auto-calculate one missing variable + // Auto-calculate one missing variable (initial user input phase) if (missing.length === 1 && filled.length === 5) { const missKey = missing[0]; - const materials = (vals.pz || 0) + (vals.tz || 0) + (vals.lz || 0) * (vals.nl || 0); + const materials = getMaterials(vals); const counts = countRoles(); const na = counts.artist; @@ -137,7 +144,6 @@ vals.nl = ((vals.s || 0) - (vals.pz || 0) - (vals.tz || 0) - (na * (vals.z || 0)) / 0.70) / (vals.lz || 0); } - // Validate non-negative if (vals[missKey] < 0) { showMessage(`Расчёт дал отрицательное значение для ${missKey.toUpperCase()}. Проверьте введённые данные.`, 'error'); updateComputed(vals.s || 0, materials, 0, 0, 0, 0); @@ -145,13 +151,47 @@ } setValue(inputs[missKey], vals[missKey]); + autoCalculated.add(missKey); + } + + // When roster changes and all fields are filled, recalc auto-calculated fields + if (fromRosterChange && missing.length === 0) { + const counts = countRoles(); + const na = counts.artist; + const materials = getMaterials(vals); + + if (na === 0) { + showMessage('Добавьте артистов', 'warning'); + updateComputed(vals.s || 0, materials, 0, 0, 0, 0); + return; + } + + if (autoCalculated.has('z') && !autoCalculated.has('s')) { + vals.z = (0.70 * ((vals.s || 0) - materials)) / na; + if (vals.z < 0) vals.z = 0; + setValue(inputs.z, vals.z); + autoCalculated.add('z'); + } else if (autoCalculated.has('s') && !autoCalculated.has('z')) { + vals.s = materials + (na * (vals.z || 0)) / 0.70; + if (vals.s < 0) vals.s = 0; + setValue(inputs.s, vals.s); + autoCalculated.add('s'); + } else if (autoCalculated.has('z') && autoCalculated.has('s')) { + // Prefer recalculating Z when both were auto-calculated + vals.z = (0.70 * ((vals.s || 0) - materials)) / na; + if (vals.z < 0) vals.z = 0; + setValue(inputs.z, vals.z); + autoCalculated.add('z'); + } else if (!autoCalculated.has('z') && !autoCalculated.has('s')) { + // Neither S nor Z was auto-calculated — warn that totals won't match + showMessage('Внимание: изменился состав артистов. Проверьте сумму S или зарплату Z.', 'warning'); + } } else if (missing.length > 1) { showMessage('Заполните все поля кроме одного, чтобы выполнить расчёт', 'info'); } - // If all filled, just recalc const allFilled = editableKeys.every(k => getValue(inputs[k]) !== null); - if (!allFilled && missing.length !== 1) { + if (!allFilled) { updateComputed(0, 0, 0, 0, 0, 0); return; } @@ -182,7 +222,7 @@ setValue(inputs.rok, rok); updateComputed(s, materials, base, totalRoyalties, salaries, counts); - updatePayouts(counts, z, rom, rop, rot, rok); + updatePayouts(counts, z, rop, rot, rok); saveState(); } @@ -209,17 +249,17 @@ } } - function updatePayouts(counts, z, rom, rop, rot, rok) { + function updatePayouts(counts, z, rop, rot, rok) { const rows = artistsTbody.querySelectorAll('tr[data-index]'); rows.forEach(row => { const idx = parseInt(row.dataset.index, 10); const artist = artists[idx]; let payout = 0; if (artist.roles.artist) payout += z; - if (artist.roles.organizer && counts.organizer > 0) payout += rom / counts.organizer; - if (artist.roles.director && counts.director > 0) payout += rop / counts.director; - if (artist.roles.technician && counts.technician > 0) payout += rot / counts.technician; - if (artist.roles.commissioner && counts.commissioner > 0) payout += rok / counts.commissioner; + // Royalties go in full to each assigned person (not split) + if (artist.roles.director) payout += rop; + if (artist.roles.technician) payout += rot; + if (artist.roles.commissioner) payout += rok; const cell = row.querySelector('.payout-cell'); if (cell) cell.textContent = round(payout).toFixed(2); }); @@ -241,7 +281,6 @@ const nl = getValue(inputs.nl) || 0; const materials = pz + tz + lz * nl; const base = s - materials; - const rom = base * 0.10; const rop = base * 0.05; const rot = base * 0.05; const rok = base * 0.10; @@ -249,10 +288,9 @@ artists.forEach((artist, idx) => { let payout = 0; if (artist.roles.artist) payout += z; - if (artist.roles.organizer && counts.organizer > 0) payout += rom / counts.organizer; - if (artist.roles.director && counts.director > 0) payout += rop / counts.director; - if (artist.roles.technician && counts.technician > 0) payout += rot / counts.technician; - if (artist.roles.commissioner && counts.commissioner > 0) payout += rok / counts.commissioner; + if (artist.roles.director) payout += rop; + if (artist.roles.technician) payout += rot; + if (artist.roles.commissioner) payout += rok; const tr = document.createElement('tr'); tr.className = 'table-row'; @@ -260,7 +298,6 @@ tr.innerHTML = ` ${escapeHtml(artist.name)} - @@ -278,22 +315,22 @@ showMessage('Этот артист уже добавлен', 'warning'); return; } - artists.push({ name, roles: { artist: true, organizer: false, director: false, technician: false, commissioner: false } }); + artists.push({ name, roles: { artist: true, director: false, technician: false, commissioner: false } }); artistSelect.value = ''; renderArtists(); - calculate(); + calculate({ fromRosterChange: true }); } function removeArtist(index) { artists.splice(index, 1); renderArtists(); - calculate(); + calculate({ fromRosterChange: true }); } function toggleRole(index, role) { artists[index].roles[role] = !artists[index].roles[role]; renderArtists(); - calculate(); + calculate({ fromRosterChange: true }); } function saveState() { @@ -307,6 +344,7 @@ z: inputs.z.value, }, artists, + autoCalculated: Array.from(autoCalculated), }; localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } @@ -328,6 +366,9 @@ artists = state.artists; renderArtists(); } + if (Array.isArray(state.autoCalculated)) { + state.autoCalculated.forEach(k => autoCalculated.add(k)); + } calculate(); } catch (e) { console.error('Failed to load state', e); @@ -345,6 +386,7 @@ z: inputs.z.value, }, artists, + autoCalculated: Array.from(autoCalculated), }; const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -372,6 +414,10 @@ inputs.nl.value = state.inputs.nl || ''; inputs.z.value = state.inputs.z || ''; artists = state.artists; + autoCalculated.clear(); + if (Array.isArray(state.autoCalculated)) { + state.autoCalculated.forEach(k => autoCalculated.add(k)); + } renderArtists(); calculate(); showMessage('Импорт завершён успешно', 'success'); @@ -395,16 +441,19 @@ inputs.rot.value = ''; inputs.rok.value = ''; artists = []; + autoCalculated.clear(); renderArtists(); calculate(); localStorage.removeItem(STORAGE_KEY); showMessage('Форма сброшена', 'info'); } - // Event listeners + // Mark manually-edited fields as no longer auto-calculated Object.values(inputs).forEach(el => { if (!el.readOnly) { - el.addEventListener('input', calculate); + el.addEventListener('input', () => { + autoCalculated.delete(el.id.replace('input-', '')); + }); el.addEventListener('change', calculate); } });