Files
calcolatore_prezzi_software/index.html
2025-09-19 23:49:09 +02:00

513 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calcolatore Prezzi Software (Italia)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
}
.card {
background-color: white;
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
transition: all 0.3s ease-in-out;
}
.card:hover {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.input-group {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.input-group label {
margin-bottom: 0.5rem;
color: #4b5563;
font-weight: 500;
}
.input-field {
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
transition: border-color 0.2s;
}
.input-field:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}
.btn-secondary {
background-color: #e5e7eb;
color: #374151;
}
.btn-secondary:hover {
background-color: #d1d5db;
}
.results-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 1024px) {
.results-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.result-box {
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
}
.result-box h3 {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.75rem;
margin-bottom: 1rem;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid #f3f4f6;
}
.result-item:last-child {
border-bottom: none;
}
.result-item-label {
color: #6b7280;
}
.result-item-value {
font-weight: 600;
color: #111827;
}
.total {
font-size: 1.25rem;
font-weight: 700;
color: #16a34a;
}
.net-total {
color: #059669;
}
.info-tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.info-tooltip .tooltip-text {
visibility: hidden;
width: 250px;
background-color: #374151;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 8px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -125px;
opacity: 0;
transition: opacity 0.3s;
font-size: 0.8rem;
font-weight: 400;
}
.info-tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
</style>
</head>
<body class="p-4 sm:p-6 lg:p-8">
<div class="max-w-7xl mx-auto">
<!-- Header -->
<header class="text-center mb-8">
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Calcolatore Prezzi Software</h1>
<p id="sub-header" class="mt-2 text-lg text-gray-600">Per Liberi Professionisti in Italia</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Colonna Sinistra: Input -->
<div class="lg:col-span-1 space-y-6">
<!-- Card Configurazione -->
<div class="card p-6">
<h2 class="text-xl font-semibold text-gray-700 border-b pb-3 mb-4">Configurazione</h2>
<div class="input-group">
<label for="taxRegime">Seleziona Tipologia Fiscale</label>
<select id="taxRegime" class="input-field bg-white">
<option value="forfettario">Regime Forfettario</option>
<option value="ordinario">Regime Ordinario (Semplificato)</option>
<option value="occasionale">Lavoro Autonomo Occasionale</option>
<option value="minimi">Regime dei Minimi (ad esaurimento)</option>
</select>
</div>
<div id="forfettario-options">
<div class="input-group">
<label for="coeffRedditivita">Coefficiente di Redditività (%)</label>
<input type="number" id="coeffRedditivita" class="input-field" value="78">
</div>
<div class="input-group">
<label for="impostaSostitutiva">Aliquota Imposta Sostitutiva</label>
<select id="impostaSostitutiva" class="input-field bg-white">
<option value="0.05">5% (Startup)</option>
<option value="0.15">15% (Standard)</option>
</select>
</div>
</div>
<div class="input-group">
<label for="devRate">Tariffa Oraria Sviluppo (€)</label>
<input type="number" id="devRate" class="input-field" value="50">
</div>
<div class="input-group">
<label for="supportRate">Tariffa Oraria Supporto (€)</label>
<input type="number" id="supportRate" class="input-field" value="40">
</div>
<div id="annualIncomeGroup" class="input-group">
<label for="estimatedAnnualTaxable" class="flex items-center">
Altri Redditi IRPEF St. (€)
<div class="info-tooltip ml-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /></svg>
<span class="tooltip-text">Inserisci altri redditi imponibili IRPEF per calcolare l'imposta marginale per questo progetto in modo più accurato.</span>
</div>
</label>
<input type="number" id="estimatedAnnualTaxable" class="input-field" value="25000">
</div>
<div class="flex items-center justify-between" id="inps-rivalsa-group">
<label for="includeINPS" class="text-gray-700 flex items-center">
Includi rivalsa INPS 4%?
<div class="info-tooltip ml-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /></svg>
<span class="tooltip-text">Aggiunge un contributo previdenziale INPS del 4% (rivalsa) alla fattura del cliente. Questo importo fa parte del tuo fatturato.</span>
</div>
</label>
<input type="checkbox" id="includeINPS" class="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500" checked>
</div>
</div>
<!-- Card MVP -->
<div class="card p-6">
<h2 class="text-xl font-semibold text-gray-700 border-b pb-3 mb-4">Prima Milestone (MVP)</h2>
<div class="input-group">
<label for="mvpDevHours">Token Sviluppo (Ore)</label>
<input type="number" id="mvpDevHours" class="input-field" value="100">
</div>
<div class="input-group">
<label for="mvpSupportHours">Token Supporto (Ore)</label>
<input type="number" id="mvpSupportHours" class="input-field" value="20">
</div>
</div>
<!-- Card Milestone Aggiuntive -->
<div class="card p-6">
<h2 class="text-xl font-semibold text-gray-700 border-b pb-3 mb-4">Milestone Aggiuntive</h2>
<div id="custom-milestones" class="space-y-4"></div>
<button id="add-milestone" class="btn btn-secondary w-full mt-4">Aggiungi Milestone</button>
</div>
</div>
<!-- Colonna Destra: Risultati -->
<div class="lg:col-span-2">
<div class="card p-6 sm:p-8">
<h2 class="text-2xl font-bold text-gray-800 mb-6">Riepilogo Preventivo e Guadagni</h2>
<div class="results-grid">
<!-- Box Preventivo Cliente -->
<div class="result-box bg-blue-50">
<h3>Preventivo per il Cliente</h3>
<div class="result-item"><span class="result-item-label" id="subtotal-label">Imponibile</span><span id="subtotal" class="result-item-value font-bold">€0.00</span></div>
<div id="inpsChargeRow" class="result-item"><span class="result-item-label">Rivalsa INPS (4%)</span><span id="inpsCharge" class="result-item-value">€0.00</span></div>
<div id="ivaRow" class="result-item"><span class="result-item-label">IVA (22%)</span><span id="iva" class="result-item-value">€0.00</span></div>
<div id="totalInvoiceRow" class="result-item"><span class="result-item-label font-bold">Totale Fattura</span><span id="totalInvoice" class="result-item-value font-bold">€0.00</span></div>
<div id="withholdingTaxRow" class="result-item"><span class="result-item-label text-red-600">Ritenuta d'acconto (20%)</span><span id="withholdingTax" class="result-item-value text-red-600">- €0.00</span></div>
<div id="amountDueRow" class="result-item mt-4"><span class="result-item-label font-bold text-lg" id="amountDueLabel">Importo dovuto dal Cliente</span><span id="amountDue" class="result-item-value total">€0.00</span></div>
</div>
<!-- Box Riepilogo Guadagni -->
<div class="result-box bg-green-50">
<h3>Riepilogo Guadagni (Stima)</h3>
<div class="result-item"><span id="grossRevenueLabel" class="result-item-label">Fatturato</span><span id="grossRevenue" class="result-item-value">€0.00</span></div>
<div class="result-item"><span class="result-item-label text-red-600">Commissione Rete Vendita (10%)</span><span id="sellerFee" class="result-item-value text-red-600">- €0.00</span></div>
<div class="result-item"><span id="netRevenueLabel" class="result-item-label">Ricavo Netto</span><span id="netRevenue" class="result-item-value font-bold">€0.00</span></div>
<div id="inpsDueRow" class="result-item"><span class="result-item-label text-red-600">Contributi INPS (~26.07%)</span><span id="inpsDue" class="result-item-value text-red-600">- €0.00</span></div>
<div class="result-item"><span id="taxDueLabel" class="result-item-label text-red-600">Imposta Dovuta</span><span id="taxDue" class="result-item-value text-red-600">- €0.00</span></div>
<div class="result-item mt-4"><span class="result-item-label font-bold text-lg">Reddito Netto Stimato</span><span id="netIncome" class="result-item-value total net-total">€0.00</span></div>
</div>
</div>
<div class="mt-6 text-xs text-gray-500 text-center">
<p><strong>Disclaimer:</strong> Questo è uno strumento di stima. Il calcolo è basato sui principi dei regimi fiscali italiani e della Gestione Separata INPS (aliquota assunta al 26,07%). Per il Lavoro Occasionale, l'INPS non è dovuta sotto i 5.000€ annui di compensi. L'IVA è considerata neutrale per il professionista. La tassazione effettiva dipende dal reddito annuo totale e dai costi deducibili. Consulta sempre un commercialista per una consulenza finanziaria accurata.</p>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const inputs = document.querySelectorAll('input, select');
const addMilestoneBtn = document.getElementById('add-milestone');
const customMilestonesContainer = document.getElementById('custom-milestones');
const taxRegimeSelector = document.getElementById('taxRegime');
const calculateIrpef = (income) => {
if (income <= 0) return 0;
let tax = 0;
const bracket1 = 28000;
const bracket2 = 50000;
if (income <= bracket1) {
tax = income * 0.23;
} else if (income <= bracket2) {
tax = (bracket1 * 0.23) + ((income - bracket1) * 0.35);
} else {
tax = (bracket1 * 0.23) + ((bracket2 - bracket1) * 0.35) + ((income - bracket2) * 0.43);
}
return tax;
};
const calculate = () => {
const regime = taxRegimeSelector.value;
const devRate = parseFloat(document.getElementById('devRate').value) || 0;
const supportRate = parseFloat(document.getElementById('supportRate').value) || 0;
const mvpDevHours = parseFloat(document.getElementById('mvpDevHours').value) || 0;
const mvpSupportHours = parseFloat(document.getElementById('mvpSupportHours').value) || 0;
const includeINPS = document.getElementById('includeINPS').checked;
let totalCustomCost = 0;
document.querySelectorAll('.custom-milestone').forEach(milestone => {
const devHours = parseFloat(milestone.querySelector('.milestone-dev-hours').value) || 0;
const supportHours = parseFloat(milestone.querySelector('.milestone-support-hours').value) || 0;
totalCustomCost += (devHours * devRate) + (supportHours * supportRate);
});
const subtotal = (mvpDevHours * devRate) + (mvpSupportHours * supportRate) + totalCustomCost;
let uiData = {};
const INPS_RATE = 0.2607;
const inpsCharge = (regime !== 'occasionale' && includeINPS) ? subtotal * 0.04 : 0;
switch(regime) {
case 'ordinario': {
const estimatedAnnualTaxable = parseFloat(document.getElementById('estimatedAnnualTaxable').value) || 0;
const ivaBase = subtotal + inpsCharge;
const iva = ivaBase * 0.22;
const totalInvoice = ivaBase + iva;
const withholdingTax = subtotal * 0.20;
const amountDue = totalInvoice - withholdingTax;
const grossRevenue = subtotal + inpsCharge;
const sellerFee = grossRevenue * 0.10;
const netRevenue = grossRevenue - sellerFee;
const inpsDue = netRevenue * INPS_RATE;
const taxableForIrpef = netRevenue - inpsDue;
const totalIrpefWithProject = calculateIrpef(estimatedAnnualTaxable + taxableForIrpef);
const irpefOnPreviousIncome = calculateIrpef(estimatedAnnualTaxable);
const taxDue = totalIrpefWithProject - irpefOnPreviousIncome;
const netIncome = netRevenue - inpsDue - taxDue;
uiData = { regime, subtotal, inpsCharge, iva, totalInvoice, withholdingTax, amountDue, grossRevenue, sellerFee, netRevenue, inpsDue, taxDue, netIncome };
break;
}
case 'forfettario': {
const coeffRedditivita = (parseFloat(document.getElementById('coeffRedditivita').value) || 0) / 100;
const impostaSostitutiva = parseFloat(document.getElementById('impostaSostitutiva').value) || 0;
const totalInvoice = subtotal + inpsCharge;
const grossRevenue = totalInvoice;
const sellerFee = grossRevenue * 0.10;
const revenueAfterFee = grossRevenue - sellerFee;
const taxableIncome = revenueAfterFee * coeffRedditivita;
const inpsDue = taxableIncome * INPS_RATE;
const taxDue = taxableIncome * impostaSostitutiva;
const netIncome = revenueAfterFee - inpsDue - taxDue;
uiData = { regime, subtotal, inpsCharge, iva: 0, totalInvoice, withholdingTax: 0, amountDue: totalInvoice, grossRevenue, sellerFee, netRevenue: revenueAfterFee, inpsDue, taxDue, netIncome };
break;
}
case 'occasionale': {
const estimatedAnnualTaxable = parseFloat(document.getElementById('estimatedAnnualTaxable').value) || 0;
const totalReceipt = subtotal; // No INPS charge, no VAT
const withholdingTax = subtotal * 0.20;
const amountDue = totalReceipt - withholdingTax;
const grossRevenue = subtotal;
const sellerFee = grossRevenue * 0.10;
const netRevenue = grossRevenue - sellerFee;
const inpsDue = 0; // Assuming under 5k threshold
const taxableForIrpef = netRevenue;
const totalIrpefWithProject = calculateIrpef(estimatedAnnualTaxable + taxableForIrpef);
const irpefOnPreviousIncome = calculateIrpef(estimatedAnnualTaxable);
const taxDue = totalIrpefWithProject - irpefOnPreviousIncome;
const netIncome = netRevenue - taxDue;
uiData = { regime, subtotal, inpsCharge: 0, iva: 0, totalInvoice: totalReceipt, withholdingTax, amountDue, grossRevenue, sellerFee, netRevenue, inpsDue, taxDue, netIncome };
break;
}
case 'minimi': {
const TAX_RATE = 0.05;
const totalInvoice = subtotal + inpsCharge;
const grossRevenue = totalInvoice;
const sellerFee = grossRevenue * 0.10;
const taxableIncome = grossRevenue - sellerFee;
const inpsDue = taxableIncome * INPS_RATE;
const taxDue = taxableIncome * TAX_RATE;
const netIncome = taxableIncome - inpsDue - taxDue;
uiData = { regime, subtotal, inpsCharge, iva: 0, totalInvoice, withholdingTax: 0, amountDue: totalInvoice, grossRevenue, sellerFee, netRevenue: taxableIncome, inpsDue, taxDue, netIncome };
break;
}
}
updateUI(uiData);
};
const updateUI = (data) => {
const formatCurrency = (value) => `${value.toFixed(2)}`;
const forfettarioOptions = document.getElementById('forfettario-options');
const annualIncomeGroup = document.getElementById('annualIncomeGroup');
const ivaRow = document.getElementById('ivaRow');
const withholdingTaxRow = document.getElementById('withholdingTaxRow');
const amountDueRow = document.getElementById('amountDueRow');
const inpsRivalsaGroup = document.getElementById('inps-rivalsa-group');
const inpsChargeRow = document.getElementById('inpsChargeRow');
const totalInvoiceRow = document.getElementById('totalInvoiceRow');
const inpsDueRow = document.getElementById('inpsDueRow');
// Reset all to hidden, then enable based on regime
forfettarioOptions.style.display = 'none';
annualIncomeGroup.style.display = 'none';
ivaRow.style.display = 'none';
withholdingTaxRow.style.display = 'none';
inpsRivalsaGroup.style.display = 'flex';
inpsChargeRow.style.display = 'flex';
totalInvoiceRow.style.display = 'flex';
inpsDueRow.style.display = 'flex';
let subHeaderText = '';
switch(data.regime) {
case 'ordinario':
annualIncomeGroup.style.display = 'flex';
ivaRow.style.display = 'flex';
withholdingTaxRow.style.display = 'flex';
document.getElementById('subtotal-label').textContent = "Imponibile";
document.getElementById('netRevenueLabel').textContent = "Ricavo Netto";
document.getElementById('taxDueLabel').textContent = "IRPEF Stimato";
document.getElementById('grossRevenueLabel').textContent = "Fatturato (Imponibile + Rivalsa)";
subHeaderText = 'Regime Ordinario (Semplificato)';
break;
case 'forfettario':
forfettarioOptions.style.display = 'block';
document.getElementById('subtotal-label').textContent = "Imponibile";
document.getElementById('netRevenueLabel').textContent = "Ricavo Post-Commissioni";
const taxRateText = (parseFloat(document.getElementById('impostaSostitutiva').value) * 100) + '%';
document.getElementById('taxDueLabel').textContent = `Imposta Sostitutiva (${taxRateText})`;
document.getElementById('grossRevenueLabel').textContent = "Fatturato";
subHeaderText = 'Regime Forfettario';
break;
case 'occasionale':
annualIncomeGroup.style.display = 'flex';
withholdingTaxRow.style.display = 'flex';
inpsRivalsaGroup.style.display = 'none';
inpsChargeRow.style.display = 'none';
totalInvoiceRow.style.display = 'none';
inpsDueRow.style.display = 'none';
document.getElementById('subtotal-label').textContent = "Compenso Lordo";
document.getElementById('amountDueLabel').textContent = "Netto a pagare";
document.getElementById('netRevenueLabel').textContent = "Ricavo Netto";
document.getElementById('taxDueLabel').textContent = "IRPEF Stimata";
document.getElementById('grossRevenueLabel').textContent = "Compenso Lordo";
subHeaderText = 'Lavoro Autonomo Occasionale';
break;
case 'minimi':
document.getElementById('subtotal-label').textContent = "Imponibile";
document.getElementById('netRevenueLabel').textContent = "Reddito Imponibile";
document.getElementById('taxDueLabel').textContent = "Imposta Sostitutiva (5%)";
document.getElementById('grossRevenueLabel').textContent = "Fatturato";
subHeaderText = 'Regime dei Minimi';
break;
}
document.getElementById('sub-header').textContent = `Per ${subHeaderText}`;
const finalAmountDue = data.amountDue;
amountDueRow.style.display = 'flex';
document.getElementById('subtotal').textContent = formatCurrency(data.subtotal);
document.getElementById('inpsCharge').textContent = formatCurrency(data.inpsCharge);
document.getElementById('iva').textContent = formatCurrency(data.iva);
document.getElementById('totalInvoice').textContent = formatCurrency(data.totalInvoice);
document.getElementById('withholdingTax').textContent = `- ${formatCurrency(data.withholdingTax)}`;
document.getElementById('amountDue').textContent = formatCurrency(finalAmountDue);
document.getElementById('grossRevenue').textContent = formatCurrency(data.grossRevenue);
document.getElementById('sellerFee').textContent = `- ${formatCurrency(data.sellerFee)}`;
document.getElementById('netRevenue').textContent = formatCurrency(data.netRevenue);
document.getElementById('inpsDue').textContent = `- ${formatCurrency(data.inpsDue)}`;
document.getElementById('taxDue').textContent = `- ${formatCurrency(data.taxDue)}`;
document.getElementById('netIncome').textContent = formatCurrency(data.netIncome);
};
inputs.forEach(input => input.addEventListener('input', calculate));
let milestoneCounter = 0;
addMilestoneBtn.addEventListener('click', () => {
milestoneCounter++;
const milestoneId = `milestone-${milestoneCounter}`;
const milestoneDiv = document.createElement('div');
milestoneDiv.className = 'custom-milestone p-4 border border-dashed rounded-lg relative';
milestoneDiv.innerHTML = `
<button class="remove-milestone absolute -top-2 -right-2 bg-red-500 text-white rounded-full h-6 w-6 flex items-center justify-center text-xs font-bold">&times;</button>
<div class="input-group">
<label class="text-sm" for="${milestoneId}-name">Nome Milestone</label>
<input type="text" id="${milestoneId}-name" class="input-field text-sm" placeholder="es. Integrazione API">
</div>
<div class="grid grid-cols-2 gap-2">
<div class="input-group mb-0">
<label class="text-sm" for="${milestoneId}-dev">Sviluppo (h)</label>
<input type="number" id="${milestoneId}-dev" class="input-field text-sm milestone-dev-hours" value="0">
</div>
<div class="input-group mb-0">
<label class="text-sm" for="${milestoneId}-support">Supporto (h)</label>
<input type="number" id="${milestoneId}-support" class="input-field text-sm milestone-support-hours" value="0">
</div>
</div>
`;
customMilestonesContainer.appendChild(milestoneDiv);
milestoneDiv.querySelectorAll('input').forEach(input => input.addEventListener('input', calculate));
milestoneDiv.querySelector('.remove-milestone').addEventListener('click', () => {
milestoneDiv.remove();
calculate();
});
});
calculate();
});
</script>
</body>
</html>