(function() {
'use strict';
const STORAGE_KEY = 'mardis_calc_state';
const inputs = {
s: document.getElementById('input-s'),
pz: document.getElementById('input-pz'),
tz: document.getElementById('input-tz'),
lz: document.getElementById('input-lz'),
nl: document.getElementById('input-nl'),
z: document.getElementById('input-z'),
na: document.getElementById('input-na'),
rom: document.getElementById('input-rom'),
rop: document.getElementById('input-rop'),
rot: document.getElementById('input-rot'),
rok: document.getElementById('input-rok'),
};
const messageEl = document.getElementById('calc-message');
const artistSelect = document.getElementById('artist-select');
const btnAddArtist = document.getElementById('btn-add-artist');
const artistsTbody = document.getElementById('artists-tbody');
const emptyRow = document.getElementById('empty-row');
const summary = {
materials: document.getElementById('summary-materials'),
base: document.getElementById('summary-base'),
royalties: document.getElementById('summary-royalties'),
salaries: document.getElementById('summary-salaries'),
s: document.getElementById('summary-s'),
};
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__ || [];
artistSelect.innerHTML = '<option value="">Выберите артиста...</option>' +
allArtistsList.map(a => `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`).join('');
} catch (e) {
console.error('Failed to load artists', e);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getValue(el) {
const v = parseFloat(el.value);
return isNaN(v) ? null : v;
}
function setValue(el, value) {
if (value === null || value === undefined) {
el.value = '';
} else {
el.value = round(value).toFixed(2);
}
}
function round(n) {
return Math.round(n * 100) / 100;
}
function countRoles() {
const counts = { artist: 0, director: 0, technician: 0, commissioner: 0 };
for (const a of artists) {
if (a.roles.artist) counts.artist++;
if (a.roles.director) counts.director++;
if (a.roles.technician) counts.technician++;
if (a.roles.commissioner) counts.commissioner++;
}
return counts;
}
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';
let vals = {
s: getValue(inputs.s),
pz: getValue(inputs.pz),
tz: getValue(inputs.tz),
lz: getValue(inputs.lz),
nl: getValue(inputs.nl),
z: getValue(inputs.z),
};
const editableKeys = ['s', 'pz', 'tz', 'lz', 'nl', 'z'];
const filled = editableKeys.filter(k => vals[k] !== null);
const missing = editableKeys.filter(k => vals[k] === null);
// Auto-calculate one missing variable (initial user input phase)
if (missing.length === 1 && filled.length === 5) {
const missKey = missing[0];
const materials = getMaterials(vals);
const counts = countRoles();
const na = counts.artist;
if (missKey === 's') {
if (na === 0 && vals.z !== null && vals.z > 0) {
showMessage('Добавьте артистов, чтобы рассчитать сумму', 'warning');
updateComputed(0, 0, 0, 0, 0, 0);
return;
}
vals.s = materials + (na * (vals.z || 0)) / 0.70;
} else if (missKey === 'z') {
if (na === 0) {
showMessage('Добавьте артистов, чтобы рассчитать зарплату', 'warning');
updateComputed(vals.s || 0, materials, 0, 0, 0, 0);
return;
}
vals.z = (0.70 * ((vals.s || 0) - materials)) / na;
} else if (missKey === 'pz') {
vals.pz = (vals.s || 0) - (vals.tz || 0) - (vals.lz || 0) * (vals.nl || 0) - (na * (vals.z || 0)) / 0.70;
} else if (missKey === 'tz') {
vals.tz = (vals.s || 0) - (vals.pz || 0) - (vals.lz || 0) * (vals.nl || 0) - (na * (vals.z || 0)) / 0.70;
} else if (missKey === 'lz') {
if ((vals.nl || 0) === 0) {
showMessage('Нельзя рассчитать Lz при Nl = 0', 'error');
updateComputed(vals.s || 0, materials, 0, 0, 0, 0);
return;
}
vals.lz = ((vals.s || 0) - (vals.pz || 0) - (vals.tz || 0) - (na * (vals.z || 0)) / 0.70) / (vals.nl || 0);
} else if (missKey === 'nl') {
if ((vals.lz || 0) === 0) {
showMessage('Нельзя рассчитать Nl при Lz = 0', 'error');
updateComputed(vals.s || 0, materials, 0, 0, 0, 0);
return;
}
vals.nl = ((vals.s || 0) - (vals.pz || 0) - (vals.tz || 0) - (na * (vals.z || 0)) / 0.70) / (vals.lz || 0);
}
if (vals[missKey] < 0) {
showMessage(`Расчёт дал отрицательное значение для ${missKey.toUpperCase()}. Проверьте введённые данные.`, 'error');
updateComputed(vals.s || 0, materials, 0, 0, 0, 0);
return;
}
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');
}
const allFilled = editableKeys.every(k => getValue(inputs[k]) !== null);
if (!allFilled) {
updateComputed(0, 0, 0, 0, 0, 0);
return;
}
const s = getValue(inputs.s) || 0;
const pz = getValue(inputs.pz) || 0;
const tz = getValue(inputs.tz) || 0;
const lz = getValue(inputs.lz) || 0;
const nl = getValue(inputs.nl) || 0;
const z = getValue(inputs.z) || 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;
const totalRoyalties = rom + rop + rot + rok;
const counts = countRoles();
const na = counts.artist;
const salaries = na * z;
setValue(inputs.na, na);
setValue(inputs.rom, rom);
setValue(inputs.rop, rop);
setValue(inputs.rot, rot);
setValue(inputs.rok, rok);
updateComputed(s, materials, base, totalRoyalties, salaries, counts);
updatePayouts(counts, z, rop, rot, rok);
saveState();
}
function showMessage(text, type) {
messageEl.textContent = text;
messageEl.hidden = false;
messageEl.className = 'alert mt-4';
if (type === 'error') messageEl.classList.add('alert-error');
else if (type === 'warning') messageEl.classList.add('alert-warning');
else if (type === 'success') messageEl.classList.add('alert-success');
else messageEl.classList.add('alert-info');
}
function updateComputed(s, materials, base, royalties, salaries, counts) {
summary.materials.textContent = round(materials).toFixed(2);
summary.base.textContent = round(base).toFixed(2);
summary.royalties.textContent = round(royalties).toFixed(2);
summary.salaries.textContent = round(salaries).toFixed(2);
summary.s.textContent = round(s).toFixed(2);
if (counts) {
const na = counts.artist;
if (na === 0 && (getValue(inputs.z) || 0) > 0) {
showMessage('Добавьте артистов в таблицу, чтобы Na соответствовало расчёту', 'warning');
}
}
}
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;
// Royalties are split equally among all people with the same role
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;
const cell = row.querySelector('.payout-cell');
if (cell) cell.textContent = round(payout).toFixed(2);
});
}
function renderArtists() {
artistsTbody.innerHTML = '';
if (artists.length === 0) {
artistsTbody.appendChild(emptyRow);
return;
}
const counts = countRoles();
const z = getValue(inputs.z) || 0;
const s = getValue(inputs.s) || 0;
const pz = getValue(inputs.pz) || 0;
const tz = getValue(inputs.tz) || 0;
const lz = getValue(inputs.lz) || 0;
const nl = getValue(inputs.nl) || 0;
const materials = pz + tz + lz * nl;
const base = s - materials;
const rop = base * 0.05;
const rot = base * 0.05;
const rok = base * 0.10;
artists.forEach((artist, idx) => {
let payout = 0;
if (artist.roles.artist) payout += z;
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;
const tr = document.createElement('tr');
tr.className = 'table-row';
tr.dataset.index = idx;
tr.innerHTML = `
<td>${escapeHtml(artist.name)}</td>
<td><label class="checkbox"><input type="checkbox" data-role="artist" ${artist.roles.artist ? 'checked' : ''}><span class="checkbox-control"></span></label></td>
<td><label class="checkbox"><input type="checkbox" data-role="director" ${artist.roles.director ? 'checked' : ''}><span class="checkbox-control"></span></label></td>
<td><label class="checkbox"><input type="checkbox" data-role="technician" ${artist.roles.technician ? 'checked' : ''}><span class="checkbox-control"></span></label></td>
<td><label class="checkbox"><input type="checkbox" data-role="commissioner" ${artist.roles.commissioner ? 'checked' : ''}><span class="checkbox-control"></span></label></td>
<td class="payout-cell">${round(payout).toFixed(2)}</td>
<td><button type="button" class="btn btn-danger btn-small" data-action="remove"><i class="ph ph-trash"></i></button></td>
`;
artistsTbody.appendChild(tr);
});
}
function addArtist() {
const name = artistSelect.value;
if (!name) return;
if (artists.some(a => a.name === name)) {
showMessage('Этот артист уже добавлен', 'warning');
return;
}
artists.push({ name, roles: { artist: true, director: false, technician: false, commissioner: false } });
artistSelect.value = '';
renderArtists();
calculate({ fromRosterChange: true });
}
function removeArtist(index) {
artists.splice(index, 1);
renderArtists();
calculate({ fromRosterChange: true });
}
function toggleRole(index, role) {
artists[index].roles[role] = !artists[index].roles[role];
renderArtists();
calculate({ fromRosterChange: true });
}
function saveState() {
const state = {
inputs: {
s: inputs.s.value,
pz: inputs.pz.value,
tz: inputs.tz.value,
lz: inputs.lz.value,
nl: inputs.nl.value,
z: inputs.z.value,
},
artists,
autoCalculated: Array.from(autoCalculated),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function loadState() {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
try {
const state = JSON.parse(raw);
if (state.inputs) {
inputs.s.value = state.inputs.s || '';
inputs.pz.value = state.inputs.pz || '';
inputs.tz.value = state.inputs.tz || '';
inputs.lz.value = state.inputs.lz || '';
inputs.nl.value = state.inputs.nl || '';
inputs.z.value = state.inputs.z || '';
}
if (Array.isArray(state.artists)) {
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);
}
}
function exportState() {
const state = {
inputs: {
s: inputs.s.value,
pz: inputs.pz.value,
tz: inputs.tz.value,
lz: inputs.lz.value,
nl: inputs.nl.value,
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);
const a = document.createElement('a');
a.href = url;
a.download = 'mardis_calc.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function importState(file) {
const reader = new FileReader();
reader.onload = () => {
try {
const state = JSON.parse(reader.result);
if (!state.inputs || !Array.isArray(state.artists)) {
throw new Error('Invalid file structure');
}
inputs.s.value = state.inputs.s || '';
inputs.pz.value = state.inputs.pz || '';
inputs.tz.value = state.inputs.tz || '';
inputs.lz.value = state.inputs.lz || '';
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');
} catch (e) {
showMessage('Ошибка импорта: неверный формат файла', 'error');
}
};
reader.readAsText(file);
}
function resetAll() {
inputs.s.value = '';
inputs.pz.value = '';
inputs.tz.value = '';
inputs.lz.value = '';
inputs.nl.value = '';
inputs.z.value = '';
inputs.na.value = '';
inputs.rom.value = '';
inputs.rop.value = '';
inputs.rot.value = '';
inputs.rok.value = '';
artists = [];
autoCalculated.clear();
renderArtists();
calculate();
localStorage.removeItem(STORAGE_KEY);
showMessage('Форма сброшена', 'info');
}
// Mark manually-edited fields as no longer auto-calculated
Object.values(inputs).forEach(el => {
if (!el.readOnly) {
el.addEventListener('input', () => {
autoCalculated.delete(el.id.replace('input-', ''));
});
el.addEventListener('change', calculate);
}
});
btnAddArtist.addEventListener('click', addArtist);
artistsTbody.addEventListener('click', e => {
const btn = e.target.closest('button[data-action="remove"]');
if (btn) {
const tr = btn.closest('tr[data-index]');
if (tr) removeArtist(parseInt(tr.dataset.index, 10));
return;
}
const checkbox = e.target.closest('input[type="checkbox"][data-role]');
if (checkbox) {
const tr = checkbox.closest('tr[data-index]');
if (tr) toggleRole(parseInt(tr.dataset.index, 10), checkbox.dataset.role);
}
});
document.getElementById('btn-export').addEventListener('click', exportState);
document.getElementById('btn-export-action').addEventListener('click', exportState);
document.getElementById('btn-reset').addEventListener('click', resetAll);
const importTrigger = document.getElementById('btn-import-trigger');
const importFile = document.getElementById('btn-import-file');
importTrigger.addEventListener('click', () => importFile.click());
importFile.addEventListener('change', e => {
if (e.target.files.length) importState(e.target.files[0]);
e.target.value = '';
});
// Init
loadArtists().then(() => {
loadState();
calculate();
});
})();