-
+
+
+
+
+
+ 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');
}
},