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