diff --git a/index.html b/index.html index 80d7253..ceff652 100644 --- a/index.html +++ b/index.html @@ -195,6 +195,92 @@ + +
+

+ + Membri del Team + INTERNO +

+ + +
+ +
+ + + + +
+

+ + Team attivo: +

+

+ Tariffa media sviluppo: | + Tariffa media supporto: +

+
+
+
@@ -356,7 +442,7 @@ Prima Milestone (MVP) -
+
+
+ + +
-
-

- Costo MVP: - -

+
+
+

+ Costo MVP: + +

+
+
+ + +
@@ -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"> -
-
+
+
-
- - +
+ + +
+

+ + Assegnazione Task +

+
+ +
+

SVILUPPO

+
+ + +
+
+ + +
+

SUPPORTO

+
+ + +
+
-
- - -
-
-
-

Costo

-

+
+ + +
+
+
+ Totale ore: + +
+
+ Costo: +
@@ -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'); } },