Przejdź do głównej treści

Kalkulator Freelancera

Obraz kalkulatora

Jesteś freelancerem i nie wiesz, jak wycenić swoją pracę? Skorzystaj z prostego, przejrzystego kalkulatora wyceny projektu, który pomoże Ci szybko ustalić uczciwą i opłacalną stawkę!

Założenia kalkulatora:

  1. Koszty życia jako punkt wyjścia
    Wprowadź swoje miesięczne koszty utrzymania (czynsz, jedzenie, transport, sprzęt itp.).
    Na ich podstawie kalkulator wyliczy Twoją minimalną stawkę godzinową, aby praca była rentowna.
    Kalkulator przyjmuje że pracujesz 160 godzin miesięcznie czyli 4x5 dni po 8 godzin.

  2. Elastyczne dodawanie zadań projektu
    Określ zakres projektu: dodawaj zadania i szacuj liczbę godzin potrzebnych do ich wykonania.

  3. Automatyczna kalkulacja ceny końcowej
    Kalkulator wyliczy całkowitą cenę projektu na podstawie sumy godzin i Twojej stawki.

  4. Szablony projektów (np. strona WWW, sklep online)
    Możesz skorzystać z gotowych przykładów projektów – wypełnią listę zadań szacunkową strukturą.

  5. Zapis danych w LocalStorage
    Twoje dane nie znikną po odświeżeniu strony – możesz wrócić do nich później.

  6. Eksport do PDF
    Wygeneruj gotową wycenę w formacie PDF – idealną do wysłania klientowi lub do archiwum.

Do czego Ci się przyda?

  • Przygotujesz ofertę dla klienta szybciej i bardziej profesjonalnie
  • Przestaniesz zaniżać ceny swojej pracy
  • Nauczysz się patrzeć na swoją stawkę przez pryzmat realnych kosztów i czasu

Zacznij już teraz

Kalkulator działa w przeglądarce, nic nie musisz instalować.
Intuicyjny interfejs + pełna kontrola nad wyceną.

Jak obsługiwać aplikację?
  • Kliknij "Dodaj koszt" i wypełnij pola
  • Kliknij "Dodaj zadanie" i wypełnij pola
  • Kliknij "Oblicz cenę projektu"
  • Kliknij "Eksportuj do PDF" jeśli chcesz wyeksportować wycenę
  • Kliknij "Zapisz dane" jeśli chcesz zapisać dane w Local Storage
1. Miesięczne koszty utrzymania
Nazwa kosztu Kwota (PLN) Akcje
2. Zadania projektu i roboczogodziny
Nazwa zadania Godziny Akcje
3. Podsumowanie

Podział kosztów utrzymania

Podział zadań w projekcie

Kod po stronie przeglądarki

<!-- Koszty utrzymania -->
<div class="card mb-4">
    <div class="card-header">1. Miesięczne koszty utrzymania</div>
    <div class="card-body table-responsive">
        <table class="table table-bordered" id="costs-table">
            <thead>
                <tr>
                    <th>Nazwa kosztu</th>
                    <th>Kwota (PLN)</th>
                    <th>Akcje</th>
                </tr>
            </thead>
        <tbody id="costs-container"></tbody>
      </table>
      <button class="btn btn-outline-primary" onclick="addCost()">
          <i class="bi bi-wallet2"></i>
          Dodaj koszt
      </button>      
    </div>
</div>

<!-- Zadania projektu -->
<div class="card mb-4">
    <div class="card-header d-flex justify-content-between align-items-center">
        <span>2. Zadania projektu i roboczogodziny</span>
        <select class="form-select w-auto" id="selectTemplate" aria-label="Wybierz szablon projektu">
            <option selected disabled>Wybierz szablon projektu</option>
            <option value="www">Strona WWW</option>
            <option value="shop">Sklep internetowy</option>
            <option value="landing">Landing page</option>
        </select>
    </div>
    <div class="card-body table-responsive">
        <table class="table table-bordered" id="tasks-table">
            <thead>
                <tr>
                    <th>Nazwa zadania</th>
                    <th>Godziny</th>
                    <th>Akcje</th>
                </tr>
            </thead>
            <tbody id="tasks-container"></tbody>
        </table>
        <button class="btn btn-outline-success" onclick="addTask()">
            <i class="bi bi-list-check"></i>
            Dodaj zadanie
        </button>      
    </div>
</div>

<!-- Wynik -->
<div class="card mb-4">
    <div class="card-header">3. Podsumowanie</div>
    <div class="card-body">
        <button class="btn btn-primary mb-3" onclick="calculate()">
            <i class="bi bi-calculator"></i>
            Oblicz cenę projektu
        </button>
        <div id="result" class="alert alert-info d-none"></div>
    </div>
</div>
  
<!-- Wykresy -->
<div class="row my-5">
    <div class="col-md-6">
        <h3 class="h5 text-center"><i class="bi bi-pie-chart"></i> Podział kosztów utrzymania</h3>
        <canvas id="costChart"></canvas>
    </div>
    <div class="col-md-6">
        <h3 class="h5 text-center"><i class="bi bi-pie-chart"></i> Podział zadań w projekcie</h3>
        <canvas id="taskChart"></canvas>
    </div>
</div>
    
<!-- Przyciski -->
<div class="btn-group" role="group">
    <button class="btn btn-outline-secondary" onclick="exportPDF()">
        <i class="bi bi-file-earmark-pdf"></i>
        Eksportuj do PDF
    </button>
    <button class="btn btn-outline-auto-darklight" onclick="saveData()">
        <i class="bi bi-save"></i>
        Zapisz dane
    </button>
    <button class="btn btn-outline-danger" onclick="clearData()">
        <i class="bi bi-trash3"></i>
        Wyczyść dane
    </button>
</div>    

<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.29/jspdf.plugin.autotable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<script>
let costChart, taskChart;

function updateCharts() {
  // Dane do wykresu kosztów
  const costLabels = [];
  const costData = [];

  for (let i = 0; i < costIndex; i++) {
    const name = document.getElementById(`cost-name-${i}`);
    const value = parseFloat(document.getElementById(`cost-value-${i}`)?.value || 0);
    if (name && value > 0) {
      costLabels.push(name.value);
      costData.push(value);
    }
  }

  // Dane do wykresu zadań
  const taskLabels = [];
  const taskData = [];

  for (let i = 0; i < taskIndex; i++) {
    const name = document.getElementById(`task-name-${i}`);
    const hours = parseFloat(document.getElementById(`task-hours-${i}`)?.value || 0);
    if (name && hours > 0) {
      taskLabels.push(name.value);
      taskData.push(hours);
    }
  }

  // Renderowanie wykresów
  if (costChart) costChart.destroy();
  if (taskChart) taskChart.destroy();

  costChart = new Chart(document.getElementById("costChart"), {
    type: "pie",
    data: {
      labels: costLabels,
      datasets: [{
        data: costData,
        backgroundColor: generateColors(costData.length),
      }]
    },
    options: {
      plugins: {
        tooltip: {
          callbacks: {
            label: function(ctx) {
              const total = ctx.chart._metasets[0].total;
              const percent = ((ctx.parsed / total) * 100).toFixed(1);
              return `${ctx.label}: ${ctx.raw} zł (${percent}%)`;
            }
          }
        }
      }
    }
  });

  taskChart = new Chart(document.getElementById("taskChart"), {
    type: "pie",
    data: {
      labels: taskLabels,
      datasets: [{
        data: taskData,
        backgroundColor: generateColors(taskData.length),
      }]
    },
    options: {
      plugins: {
        tooltip: {
          callbacks: {
            label: function(ctx) {
              const total = ctx.chart._metasets[0].total;
              const percent = ((ctx.parsed / total) * 100).toFixed(1);
              return `${ctx.label}: ${ctx.raw} h (${percent}%)`;
            }
          }
        }
      }
    }
  });
}

// Pomocnicza funkcja kolorów
function generateColors(n) {
  const baseColors = [
    "#007bff", "#28a745", "#ffc107", "#dc3545", "#17a2b8",
    "#6f42c1", "#fd7e14", "#20c997", "#e83e8c", "#6610f2"
  ];
  return Array.from({ length: n }, (_, i) => baseColors[i % baseColors.length]);
}
    
let costIndex = 0;
let taskIndex = 0;

function addCost(name = '', value = '') {
  const container = document.getElementById("costs-container");
  const row = document.createElement("tr");
  row.innerHTML = `
    <td><input type="text" class="form-control" id="cost-name-${costIndex}" value="${name}" aria-label="Nazwa kosztu ${costIndex}"></td>
    <td><input type="number" class="form-control" id="cost-value-${costIndex}" value="${value}" aria-label="Wartość kosztu ${costIndex}"></td>
    <td><button class="btn btn-sm btn-danger" onclick="this.closest('tr').remove()">Usuń</button></td>
  `;
  container.appendChild(row);
  costIndex++;  
  updateCharts();  
}

function addTask(name = '', hours = '') {
  const container = document.getElementById("tasks-container");
  const row = document.createElement("tr");
  row.innerHTML = `
    <td><input type="text" class="form-control" id="task-name-${taskIndex}" value="${name}" aria-label="Nazwa zadania ${taskIndex}"></td>
    <td><input type="number" class="form-control" id="task-hours-${taskIndex}" value="${hours}" aria-label="Czas trwania zadania ${taskIndex}"></td>
    <td><button class="btn btn-sm btn-danger" onclick="this.closest('tr').remove()">Usuń</button></td>
  `;
  container.appendChild(row);
  taskIndex++;  
  updateCharts();    
}

function calculate() {
  let totalCosts = 0;
  let totalHours = 0;

  for (let i = 0; i < costIndex; i++) {
    const val = parseFloat(document.getElementById(`cost-value-${i}`)?.value || 0);
    totalCosts += isNaN(val) ? 0 : val;
  }

  for (let i = 0; i < taskIndex; i++) {
    const val = parseFloat(document.getElementById(`task-hours-${i}`)?.value || 0);
    totalHours += isNaN(val) ? 0 : val;
  }

  const result = document.getElementById("result");

  if (totalHours === 0 || totalCosts === 0) {
    result.className = "alert alert-warning";
    result.textContent = "Dodaj co najmniej jeden koszt i jedno zadanie z godzinami.";
    result.classList.remove("d-none");
    return;
  }

  const assumedHoursPerMonth = 160;
  const hourlyRate = totalCosts / assumedHoursPerMonth;
  const projectPrice = hourlyRate * totalHours;

  result.className = "alert alert-info";
  result.innerHTML = `
    <strong>Suma kosztów miesięcznie:</strong> ${totalCosts.toFixed(2)} PLN<br>
    <strong>Suma roboczogodzin:</strong> ${totalHours} h<br>
    <strong>Stawka godzinowa:</strong> ${hourlyRate.toFixed(2)} PLN<br>
    <strong>Ostateczna cena projektu:</strong> ${projectPrice.toFixed(2)} PLN
  `;
  result.classList.remove("d-none");
  updateCharts();    
}

function applyTemplate(template) {
  const templates = {
    "www": [
      { name: "Frontend", hours: 20 },
      { name: "Backend", hours: 10 },
      { name: "Testowanie", hours: 5 }
    ],
    "shop": [
      { name: "Frontend", hours: 30 },
      { name: "Backend", hours: 20 },
      { name: "Integracja płatności", hours: 10 },
      { name: "Testy i debugowanie", hours: 5 }
    ],
    "landing": [
      { name: "Design graficzny", hours: 10 },
      { name: "Kodowanie HTML/CSS", hours: 5 },
      { name: "Optymalizacja", hours: 3 }
    ]
  };

  if (!templates[template]) return;

  // Wyczyść istniejące zadania
  document.getElementById("tasks-container").innerHTML = "";
  taskIndex = 0;

  // Dodaj z szablonu
  templates[template].forEach(task => {
    addTask(task.name, task.hours);
  });
}

async function exportPDF() {
  const { jsPDF } = window.jspdf;
  const doc = new jsPDF();

  doc.setFontSize(16);
  doc.text("Wycena projektu freelancera", 14, 20);

  // Koszty
  const costData = [];
  for (let i = 0; i < costIndex; i++) {
    const name = document.getElementById(`cost-name-${i}`)?.value || '';
    const value = document.getElementById(`cost-value-${i}`)?.value || '';
    if (name && value) costData.push([name, `${parseFloat(value).toFixed(2)} PLN`]);
  }

  if (costData.length) {
    doc.setFontSize(12);
    doc.text("Koszty miesieczne:", 14, 30);
    doc.autoTable({
      startY: 34,
      head: [["Nazwa", "Kwota"]],
      body: costData,
    });
  }

  let finalY = doc.lastAutoTable.finalY || 40;

  // Zadania
  const taskData = [];
  for (let i = 0; i < taskIndex; i++) {
    const name = document.getElementById(`task-name-${i}`)?.value || '';
    const hours = document.getElementById(`task-hours-${i}`)?.value || '';
    if (name && hours) taskData.push([name, `${hours} h`]);
  }

  if (taskData.length) {
    doc.text("Zadania projektu:", 14, finalY + 10);
    doc.autoTable({
      startY: finalY + 14,
      head: [["Zadanie", "Godziny"]],
      body: taskData,
    });
  }

  // Podsumowanie
  let totalCosts = costData.reduce((sum, row) => sum + parseFloat(row[1]), 0);
  let totalHours = taskData.reduce((sum, row) => sum + parseFloat(row[1]), 0);
  const hourlyRate = totalCosts / 160;
  const projectPrice = hourlyRate * totalHours;

  finalY = doc.lastAutoTable.finalY || finalY + 30;

  doc.setFontSize(12);
  doc.text("Podsumowanie:", 14, finalY + 10);
  doc.setFontSize(10);
  doc.text(`Suma kosztów: ${totalCosts.toFixed(2)} PLN`, 14, finalY + 18);
  doc.text(`Suma roboczogodzin: ${totalHours.toFixed(2)} h`, 14, finalY + 26);
  doc.text(`Stawka godzinowa: ${hourlyRate.toFixed(2)} PLN`, 14, finalY + 34);
  doc.text(`Cena projektu: ${projectPrice.toFixed(2)} PLN`, 14, finalY + 42);

  // Zapisz PDF
  doc.save("wycena-projektu.pdf");
}
function saveData() {
  const costs = [];
  const tasks = [];
    
  for (let i = 0; i < costIndex; i++) {
    const name = document.getElementById(`cost-name-${i}`);
    const value = document.getElementById(`cost-value-${i}`);
    if (name && value) {
      costs.push({ name: name.value, value: value.value });
    }
  }

  for (let i = 0; i < taskIndex; i++) {
    const name = document.getElementById(`task-name-${i}`);
    const hours = document.getElementById(`task-hours-${i}`);
    if (name && hours) {
      tasks.push({ name: name.value, hours: hours.value });
    }
  }
    
  localStorage.setItem("freelancerCosts", JSON.stringify(costs));
  localStorage.setItem("freelancerTasks", JSON.stringify(tasks));  
    
  BootstrapToast.show({title: 'Powiadomienie', message: 'Dane zostały zapisane.', when: 'teraz', type: 'text-bg-success'});
}
function clearData() {      
  localStorage.removeItem("freelancerCosts");
  localStorage.removeItem("freelancerTasks");  
    
  BootstrapToast.show({title: 'Powiadomienie', message: 'Dane zostały wyczyszczone. Za chwilę nastąpi przeładowanie strony...', when: 'teraz', type: 'text-bg-danger'});
  setTimeout(function() {
      window.location.reload();
  }, 3000);  
}
    
function loadData() {
  const savedCosts = JSON.parse(localStorage.getItem("freelancerCosts")) || [];
  const savedTasks = JSON.parse(localStorage.getItem("freelancerTasks")) || [];

  savedCosts.forEach(c => addCost(c.name, c.value));
  savedTasks.forEach(t => addTask(t.name, t.hours));
  updateCharts();    
}

const select = document.getElementById('selectTemplate');
select.addEventListener('change', function() {
    let value = select.value;
    applyTemplate(value);
})    
document.addEventListener('DOMContentLoaded', loadData);    
</script>

Kod po stronie serwera

Informacja

Aplikacja nie korzysta z kodu po stronie serwera.

Tagi

Javascript Freelance

Dziękujemy!
()

Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.