Update index.html
All checks were successful
Build and Deploy / build (push) Successful in 24s

This commit is contained in:
2025-09-24 00:15:23 +02:00
parent 76a8a576e3
commit 4dba11f303

View File

@@ -195,6 +195,92 @@
</div>
</div>
<!-- Team Members -->
<div class="glass rounded-2xl p-6 mb-6 animate-slide-in">
<h2 class="text-2xl font-bold text-gray-800 mb-4 flex items-center gap-2">
<i class="fas fa-users text-blue-600"></i>
Membri del Team
<span class="bg-orange-500 text-white text-xs px-2 py-1 rounded-full ml-2">INTERNO</span>
</h2>
<!-- Team member list -->
<div class="space-y-3 mb-4">
<template x-for="(member, index) in teamMembers" :key="member.id">
<div class="p-4 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border-2 border-indigo-200">
<div class="flex items-start justify-between">
<div class="flex-1 grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Nome</label>
<input type="text" x-model="member.name"
class="w-full p-2 text-sm border-2 border-indigo-200 rounded-lg focus:border-indigo-500 focus:outline-none"
placeholder="Nome membro">
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Ruolo</label>
<select x-model="member.role"
class="w-full p-2 text-sm border-2 border-indigo-200 rounded-lg focus:border-indigo-500 focus:outline-none">
<option value="developer">Developer</option>
<option value="senior">Senior Developer</option>
<option value="architect">Software Architect</option>
<option value="designer">UI/UX Designer</option>
<option value="pm">Project Manager</option>
<option value="analyst">Business Analyst</option>
<option value="tester">QA Tester</option>
<option value="support">Support</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Tariffa Sviluppo (€/h)</label>
<input type="number" x-model.number="member.devRate"
class="w-full p-2 text-sm border-2 border-indigo-200 rounded-lg focus:border-indigo-500 focus:outline-none"
step="5">
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Tariffa Supporto (€/h)</label>
<input type="number" x-model.number="member.supportRate"
class="w-full p-2 text-sm border-2 border-indigo-200 rounded-lg focus:border-indigo-500 focus:outline-none"
step="5">
</div>
</div>
<button @click="removeTeamMember(index)"
class="ml-3 text-red-500 hover:text-red-700 transition-colors">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="mt-3 flex items-center gap-4">
<div class="flex items-center">
<input type="checkbox" :id="'active-' + member.id" x-model="member.active"
class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
<label :for="'active-' + member.id" class="ml-2 text-sm text-gray-700">Attivo nel progetto</label>
</div>
<div class="text-sm text-gray-600">
<i class="fas fa-calculator text-indigo-500"></i>
<span x-text="'Giornata (8h): €' + (member.devRate * 8)"></span>
</div>
</div>
</div>
</template>
</div>
<button @click="addTeamMember()"
class="w-full bg-gradient-to-r from-indigo-500 to-purple-500 text-white font-bold py-3 px-6 rounded-lg hover:from-indigo-600 hover:to-purple-600 transition-all flex items-center justify-center gap-2">
<i class="fas fa-user-plus"></i>
Aggiungi Membro del Team
</button>
<!-- Team summary -->
<div x-show="teamMembers.filter(m => m.active).length > 0" class="mt-4 p-4 bg-indigo-50 rounded-lg">
<p class="text-sm text-gray-700 mb-2">
<i class="fas fa-users text-indigo-600 mr-2"></i>
Team attivo: <span class="font-bold" x-text="teamMembers.filter(m => m.active).length + ' membri'"></span>
</p>
<p class="text-xs text-gray-600">
Tariffa media sviluppo: <span class="font-bold text-indigo-600" x-text="'€' + calculateAverageDevRate().toFixed(2) + '/h'"></span> |
Tariffa media supporto: <span class="font-bold text-purple-600" x-text="'€' + calculateAverageSupportRate().toFixed(2) + '/h'"></span>
</p>
</div>
</div>
<!-- Tabs Configurazione -->
<div class="glass rounded-2xl p-6 mb-6 animate-slide-in">
<!-- Tab Headers -->
@@ -356,7 +442,7 @@
<i class="fas fa-flag text-green-600"></i>
Prima Milestone (MVP)
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Ore Sviluppo</label>
<input type="number" x-model.number="mvpDevHours"
@@ -369,12 +455,28 @@
class="w-full p-3 border-2 border-green-200 rounded-lg focus:border-green-500 focus:outline-none"
step="1">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Team Leader</label>
<select x-model="mvpTeamLeader"
class="w-full p-3 border-2 border-green-200 rounded-lg focus:border-green-500 focus:outline-none">
<option value="">Tariffa Standard</option>
<template x-for="member in teamMembers.filter(m => m.active)" :key="member.id">
<option :value="member.id" x-text="member.name || 'Membro ' + member.id"></option>
</template>
</select>
</div>
</div>
<div class="mt-3 text-green-700">
<p class="font-semibold">
Costo MVP:
<span class="text-xl" x-text="'€ ' + ((mvpDevHours * devRate) + (mvpSupportHours * supportRate)).toFixed(2)"></span>
</p>
<div class="mt-3 flex justify-between items-center">
<div class="text-green-700">
<p class="font-semibold">
Costo MVP:
<span class="text-xl" x-text="'€ ' + calculateMvpCost().toFixed(2)"></span>
</p>
</div>
<div x-show="mvpTeamLeader" class="text-sm text-green-600">
<i class="fas fa-user-tie mr-1"></i>
<span x-text="'Assegnato a: ' + getTeamMemberName(mvpTeamLeader)"></span>
</div>
</div>
</div>
@@ -386,29 +488,92 @@
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full h-8 w-8 flex items-center justify-center hover:bg-red-600 transition-colors">
<i class="fas fa-times"></i>
</button>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="md:col-span-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-2">Nome Milestone</label>
<input type="text" x-model="milestone.name"
class="w-full p-3 border-2 border-blue-200 rounded-lg focus:border-blue-500 focus:outline-none"
placeholder="es. Integrazione API">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Ore Sviluppo</label>
<input type="number" x-model.number="milestone.devHours"
class="w-full p-3 border-2 border-blue-200 rounded-lg focus:border-blue-500 focus:outline-none"
step="1">
</div>
<!-- Task assignments for milestone -->
<div class="space-y-2">
<p class="text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-user-friends text-blue-500 mr-1"></i>
Assegnazione Task
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Sviluppo -->
<div class="p-3 bg-white rounded-lg border border-blue-200">
<p class="text-xs font-semibold text-blue-700 mb-2">SVILUPPO</p>
<div class="space-y-2">
<template x-for="(task, taskIndex) in milestone.devTasks" :key="task.id">
<div class="flex items-center gap-2">
<select x-model="task.memberId"
class="flex-1 p-2 text-sm border border-gray-300 rounded">
<option value="">Tariffa Standard</option>
<template x-for="member in teamMembers.filter(m => m.active)" :key="member.id">
<option :value="member.id" x-text="member.name || 'Membro ' + member.id"></option>
</template>
</select>
<input type="number" x-model.number="task.hours"
class="w-20 p-2 text-sm border border-gray-300 rounded"
placeholder="Ore" step="1">
<button @click="milestone.devTasks.splice(taskIndex, 1)"
class="text-red-500 hover:text-red-700">
<i class="fas fa-minus-circle"></i>
</button>
</div>
</template>
<button @click="addDevTask(milestone)"
class="w-full py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors">
<i class="fas fa-plus mr-1"></i> Aggiungi Task Sviluppo
</button>
</div>
</div>
<!-- Supporto -->
<div class="p-3 bg-white rounded-lg border border-purple-200">
<p class="text-xs font-semibold text-purple-700 mb-2">SUPPORTO</p>
<div class="space-y-2">
<template x-for="(task, taskIndex) in milestone.supportTasks" :key="task.id">
<div class="flex items-center gap-2">
<select x-model="task.memberId"
class="flex-1 p-2 text-sm border border-gray-300 rounded">
<option value="">Tariffa Standard</option>
<template x-for="member in teamMembers.filter(m => m.active)" :key="member.id">
<option :value="member.id" x-text="member.name || 'Membro ' + member.id"></option>
</template>
</select>
<input type="number" x-model.number="task.hours"
class="w-20 p-2 text-sm border border-gray-300 rounded"
placeholder="Ore" step="1">
<button @click="milestone.supportTasks.splice(taskIndex, 1)"
class="text-red-500 hover:text-red-700">
<i class="fas fa-minus-circle"></i>
</button>
</div>
</template>
<button @click="addSupportTask(milestone)"
class="w-full py-1 text-xs bg-purple-100 text-purple-700 rounded hover:bg-purple-200 transition-colors">
<i class="fas fa-plus mr-1"></i> Aggiungi Task Supporto
</button>
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Ore Supporto</label>
<input type="number" x-model.number="milestone.supportHours"
class="w-full p-3 border-2 border-blue-200 rounded-lg focus:border-blue-500 focus:outline-none"
step="1">
</div>
<div class="flex items-end">
<div class="w-full p-3 bg-white rounded-lg text-center">
<p class="text-sm text-gray-600">Costo</p>
<p class="font-bold text-blue-600" x-text="'€ ' + ((milestone.devHours * devRate) + (milestone.supportHours * supportRate)).toFixed(2)"></p>
</div>
<!-- Milestone summary -->
<div class="mt-3 pt-3 border-t border-blue-200">
<div class="flex justify-between items-center">
<div>
<span class="text-sm text-gray-600">Totale ore: </span>
<span class="font-bold text-blue-600" x-text="calculateMilestoneHours(milestone) + 'h'"></span>
</div>
<div>
<span class="text-sm text-gray-600">Costo: </span>
<span class="font-bold text-blue-600 text-lg" x-text="'€ ' + calculateMilestoneCost(milestone).toFixed(2)"></span>
</div>
</div>
</div>
@@ -627,6 +792,10 @@
data: new Date().toISOString().split('T')[0]
},
// Team members
teamMembers: [],
teamMemberCounter: 0,
taxRegime: 'forfettario',
coeffRedditivita: 78,
impostaSostitutiva: 0.15,
@@ -636,14 +805,136 @@
includeINPS: true,
mvpDevHours: 100,
mvpSupportHours: 20,
mvpTeamLeader: '',
milestones: [],
milestoneCounter: 0,
taskCounter: 0,
// Constants
INPS_RATE: 0.2607,
init() {
this.caricaPreventiviDaStorage();
// Initialize with one default team member
this.addTeamMember();
},
// Team member management
addTeamMember() {
this.teamMemberCounter++;
this.teamMembers.push({
id: this.teamMemberCounter,
name: '',
role: 'developer',
devRate: this.devRate,
supportRate: this.supportRate,
active: true
});
},
removeTeamMember(index) {
this.teamMembers.splice(index, 1);
},
getTeamMemberName(memberId) {
const member = this.teamMembers.find(m => m.id == memberId);
return member ? (member.name || `Membro ${member.id}`) : 'Non assegnato';
},
getTeamMemberRate(memberId, type = 'dev') {
if (!memberId) {
return type === 'dev' ? this.devRate : this.supportRate;
}
const member = this.teamMembers.find(m => m.id == memberId);
if (member) {
return type === 'dev' ? member.devRate : member.supportRate;
}
return type === 'dev' ? this.devRate : this.supportRate;
},
calculateAverageDevRate() {
const activeMembers = this.teamMembers.filter(m => m.active);
if (activeMembers.length === 0) return this.devRate;
const sum = activeMembers.reduce((acc, m) => acc + m.devRate, 0);
return sum / activeMembers.length;
},
calculateAverageSupportRate() {
const activeMembers = this.teamMembers.filter(m => m.active);
if (activeMembers.length === 0) return this.supportRate;
const sum = activeMembers.reduce((acc, m) => acc + m.supportRate, 0);
return sum / activeMembers.length;
},
calculateMvpCost() {
const devRate = this.getTeamMemberRate(this.mvpTeamLeader, 'dev');
const supportRate = this.getTeamMemberRate(this.mvpTeamLeader, 'support');
return (this.mvpDevHours * devRate) + (this.mvpSupportHours * supportRate);
},
// Task management for milestones
addDevTask(milestone) {
this.taskCounter++;
if (!milestone.devTasks) milestone.devTasks = [];
milestone.devTasks.push({
id: this.taskCounter,
memberId: '',
hours: 0
});
},
addSupportTask(milestone) {
this.taskCounter++;
if (!milestone.supportTasks) milestone.supportTasks = [];
milestone.supportTasks.push({
id: this.taskCounter,
memberId: '',
hours: 0
});
},
calculateMilestoneHours(milestone) {
let totalHours = 0;
if (milestone.devTasks) {
totalHours += milestone.devTasks.reduce((sum, task) => sum + (task.hours || 0), 0);
}
if (milestone.supportTasks) {
totalHours += milestone.supportTasks.reduce((sum, task) => sum + (task.hours || 0), 0);
}
// Legacy support for old milestone format
if (milestone.devHours) totalHours += milestone.devHours;
if (milestone.supportHours) totalHours += milestone.supportHours;
return totalHours;
},
calculateMilestoneCost(milestone) {
let totalCost = 0;
// Calculate dev tasks cost
if (milestone.devTasks) {
milestone.devTasks.forEach(task => {
const rate = this.getTeamMemberRate(task.memberId, 'dev');
totalCost += (task.hours || 0) * rate;
});
}
// Calculate support tasks cost
if (milestone.supportTasks) {
milestone.supportTasks.forEach(task => {
const rate = this.getTeamMemberRate(task.memberId, 'support');
totalCost += (task.hours || 0) * rate;
});
}
// Legacy support for old milestone format
if (milestone.devHours) {
totalCost += milestone.devHours * this.devRate;
}
if (milestone.supportHours) {
totalCost += milestone.supportHours * this.supportRate;
}
return totalCost;
},
calculateProgress() {
@@ -737,13 +1028,15 @@
// Calculation results
get results() {
// Calculate MVP cost with team member rates
const mvpCost = this.calculateMvpCost();
// Calculate milestones cost with team member rates
const totalCustomCost = this.milestones.reduce((sum, milestone) => {
return sum + (milestone.devHours * this.devRate) + (milestone.supportHours * this.supportRate);
return sum + this.calculateMilestoneCost(milestone);
}, 0);
const subtotal = (this.mvpDevHours * this.devRate) +
(this.mvpSupportHours * this.supportRate) +
totalCustomCost;
const subtotal = mvpCost + totalCustomCost;
const inpsCharge = (this.taxRegime !== 'occasionale' && this.includeINPS)
? subtotal * 0.04
@@ -876,18 +1169,33 @@
},
calcolaOreTotali() {
return this.mvpDevHours + this.mvpSupportHours +
this.milestones.reduce((sum, m) => sum + m.devHours + m.supportHours, 0);
let totale = this.mvpDevHours + this.mvpSupportHours;
this.milestones.forEach(m => {
totale += this.calculateMilestoneHours(m);
});
return totale;
},
calcolaOreSviluppo() {
return this.mvpDevHours +
this.milestones.reduce((sum, m) => sum + m.devHours, 0);
let totale = this.mvpDevHours;
this.milestones.forEach(m => {
if (m.devTasks) {
totale += m.devTasks.reduce((sum, task) => sum + (task.hours || 0), 0);
}
if (m.devHours) totale += m.devHours; // Legacy support
});
return totale;
},
calcolaOreSupporto() {
return this.mvpSupportHours +
this.milestones.reduce((sum, m) => sum + m.supportHours, 0);
let totale = this.mvpSupportHours;
this.milestones.forEach(m => {
if (m.supportTasks) {
totale += m.supportTasks.reduce((sum, task) => sum + (task.hours || 0), 0);
}
if (m.supportHours) totale += m.supportHours; // Legacy support
});
return totale;
},
addMilestone() {
@@ -895,9 +1203,12 @@
this.milestones.push({
id: this.milestoneCounter,
name: '',
devHours: 0,
supportHours: 0
devTasks: [],
supportTasks: []
});
// Add default tasks
this.addDevTask(this.milestones[this.milestones.length - 1]);
this.addSupportTask(this.milestones[this.milestones.length - 1]);
},
removeMilestone(index) {
@@ -908,6 +1219,7 @@
const dati = {
azienda: this.azienda,
cliente: this.cliente,
teamMembers: this.teamMembers,
taxRegime: this.taxRegime,
coeffRedditivita: this.coeffRedditivita,
impostaSostitutiva: this.impostaSostitutiva,
@@ -917,6 +1229,7 @@
includeINPS: this.includeINPS,
mvpDevHours: this.mvpDevHours,
mvpSupportHours: this.mvpSupportHours,
mvpTeamLeader: this.mvpTeamLeader,
milestones: this.milestones,
totali: this.results,
timestamp: new Date().toISOString()
@@ -954,6 +1267,7 @@
if (preventivo) {
this.azienda = preventivo.azienda || this.azienda;
this.cliente = preventivo.cliente || this.cliente;
this.teamMembers = preventivo.teamMembers || [];
this.taxRegime = preventivo.taxRegime || this.taxRegime;
this.coeffRedditivita = preventivo.coeffRedditivita || this.coeffRedditivita;
this.impostaSostitutiva = preventivo.impostaSostitutiva || this.impostaSostitutiva;
@@ -963,6 +1277,7 @@
this.includeINPS = preventivo.includeINPS !== undefined ? preventivo.includeINPS : this.includeINPS;
this.mvpDevHours = preventivo.mvpDevHours || this.mvpDevHours;
this.mvpSupportHours = preventivo.mvpSupportHours || this.mvpSupportHours;
this.mvpTeamLeader = preventivo.mvpTeamLeader || '';
this.milestones = preventivo.milestones || [];
this.showLoadModal = false;
this.showNotification('Preventivo caricato con successo!', 'success');
@@ -987,11 +1302,16 @@
data: new Date().toISOString().split('T')[0]
};
this.teamMembers = [];
this.mvpDevHours = 100;
this.mvpSupportHours = 20;
this.mvpTeamLeader = '';
this.milestones = [];
this.estimatedAnnualTaxable = 25000;
// Re-add default team member
this.addTeamMember();
this.showNotification('Form resettato!', 'info');
}
},