Newer
Older
mardis_calc / js / app.js
(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, organizer, director, technician, commissioner } }]
  let allArtistsList = [];

  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, organizer: 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++;
    }
    return counts;
  }

  function calculate() {
    messageEl.textContent = '';
    messageEl.className = 'input-info mt-4';

    const 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
    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 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);
      }

      // Validate non-negative
      if (vals[missKey] < 0) {
        showMessage(`Расчёт дал отрицательное значение для ${missKey.toUpperCase()}. Проверьте введённые данные.`, 'error');
        updateComputed(vals.s || 0, materials, 0, 0, 0, 0);
        return;
      }

      setValue(inputs[missKey], vals[missKey]);
    } 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) {
      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, rom, rop, rot, rok);
    saveState();
  }

  function showMessage(text, type) {
    messageEl.textContent = text;
    messageEl.className = 'input-info mt-4';
    if (type === 'error') messageEl.classList.add('error');
    if (type === 'warning') messageEl.classList.add('warning');
    if (type === 'success') messageEl.classList.add('success');
  }

  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, rom, 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;
      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 rom = base * 0.10;
    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.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;

      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="organizer" ${artist.roles.organizer ? '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, organizer: false, 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,
    };
    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();
      }
      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,
    };
    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;
        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 = [];
    renderArtists();
    calculate();
    localStorage.removeItem(STORAGE_KEY);
    showMessage('Форма сброшена', 'info');
  }

  // Event 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-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();
  });
})();