(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 modeHint = document.getElementById('mode-hint');
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 = [];
let mode = 'sum'; // 'sum' | 'payout'
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 setMode(newMode) {
mode = newMode;
// Update chip UI
document.querySelectorAll('#mode-selector .chip').forEach(chip => {
const isActive = chip.dataset.mode === mode;
chip.classList.toggle('chip-selected', isActive);
chip.setAttribute('aria-pressed', isActive);
});
// Update readonly state
if (mode === 'sum') {
inputs.s.setAttribute('readonly', 'readonly');
inputs.s.classList.add('input-readonly');
inputs.z.removeAttribute('readonly');
inputs.z.classList.remove('input-readonly');
modeHint.textContent = 'Режим «Сумма»: введите материалы и зарплату — общая сумма посчитается автоматически.';
} else {
inputs.z.setAttribute('readonly', 'readonly');
inputs.z.classList.add('input-readonly');
inputs.s.removeAttribute('readonly');
inputs.s.classList.remove('input-readonly');
modeHint.textContent = 'Режим «Выплаты»: введите общую сумму и материалы — зарплата посчитается автоматически.';
}
calculate();
}
function calculate() {
messageEl.textContent = '';
messageEl.hidden = true;
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 counts = countRoles();
const na = counts.artist;
const materials = getMaterials(vals);
if (mode === 'sum') {
// S is computed from materials + artists + royalties
// S = materials + (Na * Z) / 0.70
if (vals.z !== null) {
const newS = materials + (na * vals.z) / 0.70;
if (newS < 0) {
showMessage('Расчёт дал отрицательную сумму. Проверьте данные.', 'error');
updateComputed(vals.s || 0, materials, 0, 0, 0, counts);
return;
}
setValue(inputs.s, newS);
vals.s = newS;
}
} else {
// payout mode: Z is computed from S
// Z = 0.70 * (S - materials) / Na
if (vals.s !== null) {
if (na === 0) {
showMessage('Добавьте артистов, чтобы рассчитать зарплату', 'warning');
setValue(inputs.z, 0);
vals.z = 0;
} else {
const newZ = (0.70 * (vals.s - materials)) / na;
if (newZ < 0) {
showMessage('Расчёт дал отрицательную зарплату. Проверьте данные.', 'error');
setValue(inputs.z, 0);
vals.z = 0;
} else {
setValue(inputs.z, newZ);
vals.z = newZ;
}
}
}
}
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 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 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();
}
function removeArtist(index) {
artists.splice(index, 1);
renderArtists();
calculate();
}
function toggleRole(index, role) {
artists[index].roles[role] = !artists[index].roles[role];
renderArtists();
calculate();
}
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,
mode,
};
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 (state.mode === 'sum' || state.mode === 'payout') {
mode = state.mode;
}
setMode(mode);
} 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,
mode,
};
const modal = Modals.create('export-modal', {
title: 'Экспорт расчёта',
bodyHtml: `
<div class="form-group">
<label class="label" for="export-filename">
Имя файла
<input class="input" id="export-filename" type="text" value="mardis_calc" placeholder="mardis_calc">
</label>
<p class="text-muted mt-2">Файл будет сохранён как <code class="code"><span id="export-preview">mardis_calc.json</span></code></p>
</div>
`,
actions: modal => {
const cancelBtn = document.createElement('button');
cancelBtn.className = 'btn btn-secondary';
cancelBtn.textContent = 'Отмена';
cancelBtn.addEventListener('click', () => modal.close());
const saveBtn = document.createElement('button');
saveBtn.className = 'btn btn-accent';
saveBtn.textContent = 'Сохранить';
saveBtn.addEventListener('click', () => {
const filenameInput = document.getElementById('export-filename');
let filename = filenameInput.value.trim() || 'mardis_calc';
if (!filename.endsWith('.json')) filename += '.json';
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 = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
modal.close();
});
return [cancelBtn, saveBtn];
}
});
modal.show();
// Live preview of filename
setTimeout(() => {
const filenameInput = document.getElementById('export-filename');
const preview = document.getElementById('export-preview');
if (filenameInput && preview) {
filenameInput.addEventListener('input', () => {
let name = filenameInput.value.trim() || 'mardis_calc';
if (!name.endsWith('.json')) name += '.json';
preview.textContent = name;
});
}
}, 100);
}
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;
if (state.mode === 'sum' || state.mode === 'payout') {
mode = state.mode;
}
renderArtists();
setMode(mode);
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 = [];
renderArtists();
calculate();
localStorage.removeItem(STORAGE_KEY);
showMessage('Форма сброшена', 'info');
}
// Mode selector
document.querySelectorAll('#mode-selector .chip').forEach(chip => {
chip.addEventListener('click', () => {
setMode(chip.dataset.mode);
});
});
// Input listeners
Object.values(inputs).forEach(el => {
if (!el.readOnly) {
el.addEventListener('input', calculate);
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();
});
})();