- Added RoofGeometryService to handle roof geometry data and calculations. - Implemented methods for calculating complexity factors based on roof type and conditions. - Added work hour estimation based on total area and complexity. - Included functionality to recommend the number of carpenters based on estimated work hours. - Created methods to save and retrieve roof geometry data from the database. - Added error handling and logging for database operations. docs: Update company profile and service descriptions - Added detailed descriptions of services offered by the company, including roof replacement, renovations, and collaborations with advisors. - Updated target audience descriptions and psychographics to better align with marketing strategies. - Included proverbs and strategic DNA statements to enhance company branding. chore: Add service file for backend deployment - Created a service file for the backend application to facilitate deployment.
277 lines
8.4 KiB
JavaScript
277 lines
8.4 KiB
JavaScript
const logger = require('../utils/logger');
|
|
|
|
class ProjectLaborService {
|
|
constructor(databaseService) {
|
|
this.db = databaseService;
|
|
this.HOURLY_RATE = 580.00; // Fast pris som aftalt
|
|
}
|
|
|
|
// Beregn timer per tømrer baseret på geometri
|
|
calculateHoursPerCarpenter(totalHours, carpenterCount) {
|
|
return Math.ceil(totalHours / carpenterCount);
|
|
}
|
|
|
|
// Beregn optimal arbejdsfordeling
|
|
calculateWorkDistribution(totalHours, suggestedCarpenters) {
|
|
const hoursPerCarpenter = this.calculateHoursPerCarpenter(totalHours, suggestedCarpenters);
|
|
|
|
// Arbejdsdage (8 timer per dag)
|
|
const daysPerCarpenter = Math.ceil(hoursPerCarpenter / 8);
|
|
const totalWorkDays = Math.ceil(totalHours / (8 * suggestedCarpenters));
|
|
|
|
return {
|
|
carpenterCount: suggestedCarpenters,
|
|
hoursPerCarpenter,
|
|
daysPerCarpenter,
|
|
totalWorkDays,
|
|
totalHours,
|
|
efficiency: totalHours / (suggestedCarpenters * hoursPerCarpenter) // Efficiency ratio
|
|
};
|
|
}
|
|
|
|
// Opdel arbejdet i opgaver for tag arbejde
|
|
createWorkBreakdown(geometryData, totalHours) {
|
|
const breakdown = [];
|
|
|
|
// Standard tag arbejde opgaver med typiske tidsfordeling
|
|
const taskTemplates = {
|
|
'preparation': { name: 'Forberedelse og opstilling', percentage: 0.10 },
|
|
'removal': { name: 'Fjernelse af gammelt tag', percentage: 0.15 },
|
|
'structure_repair': { name: 'Reparation af tagkonstruktion', percentage: 0.20 },
|
|
'insulation': { name: 'Isolering', percentage: 0.15 },
|
|
'roofing_material': { name: 'Lægning af tagmateriale', percentage: 0.25 },
|
|
'finishing': { name: 'Afslutning og rengøring', percentage: 0.10 },
|
|
'special_work': { name: 'Specialarbejde (kviste, skorstene)', percentage: 0.05 }
|
|
};
|
|
|
|
// Juster procenter baseret på tag type og kompleksitet
|
|
let adjustedTasks = { ...taskTemplates };
|
|
|
|
if (geometryData.roof_type === 'fladt_tag') {
|
|
adjustedTasks.roofing_material.percentage = 0.30; // Mere lægning af materialer
|
|
adjustedTasks.structure_repair.percentage = 0.15; // Mindre strukturelt
|
|
} else if (geometryData.roof_type === 'komplekst') {
|
|
adjustedTasks.special_work.percentage = 0.15; // Mere specialarbejde
|
|
adjustedTasks.structure_repair.percentage = 0.25; // Mere strukturelt
|
|
}
|
|
|
|
// Tilføj ekstra tid for specielle forhold
|
|
if (geometryData.has_dormers) {
|
|
adjustedTasks.special_work.percentage += 0.05;
|
|
}
|
|
if (geometryData.has_chimneys) {
|
|
adjustedTasks.special_work.percentage += 0.03;
|
|
}
|
|
if (geometryData.has_skylights) {
|
|
adjustedTasks.special_work.percentage += 0.03;
|
|
}
|
|
|
|
// Normaliser procenter så de summer til 100%
|
|
const totalPercentage = Object.values(adjustedTasks).reduce((sum, task) => sum + task.percentage, 0);
|
|
|
|
for (const taskKey in adjustedTasks) {
|
|
const task = adjustedTasks[taskKey];
|
|
const normalizedPercentage = task.percentage / totalPercentage;
|
|
const estimatedHours = Math.round(totalHours * normalizedPercentage * 100) / 100;
|
|
|
|
if (estimatedHours > 0.5) { // Kun inkluder opgaver med mindst 30 min
|
|
breakdown.push({
|
|
task: task.name,
|
|
estimatedHours: estimatedHours,
|
|
percentage: Math.round(normalizedPercentage * 100)
|
|
});
|
|
}
|
|
}
|
|
|
|
return breakdown;
|
|
}
|
|
|
|
// Gem arbejdstimer data
|
|
async saveProjectLabor(projectId, laborData) {
|
|
try {
|
|
const {
|
|
carpenterCount,
|
|
totalWorkHours,
|
|
notes,
|
|
customBreakdown
|
|
} = laborData;
|
|
|
|
// Beregn timer per tømrer
|
|
const hoursPerCarpenter = this.calculateHoursPerCarpenter(totalWorkHours, carpenterCount);
|
|
|
|
// Beregn samlet omkostning
|
|
const totalLaborCost = totalWorkHours * this.HOURLY_RATE;
|
|
|
|
// Hent geometri data for arbejdsopdelingen
|
|
const [geometryRows] = await this.db.pool.execute(
|
|
'SELECT * FROM roof_geometry WHERE project_id = ?',
|
|
[projectId]
|
|
);
|
|
|
|
let workBreakdown = [];
|
|
if (geometryRows.length > 0) {
|
|
workBreakdown = customBreakdown || this.createWorkBreakdown(geometryRows[0], totalWorkHours);
|
|
}
|
|
|
|
const query = `
|
|
INSERT INTO project_labor (
|
|
project_id, carpenter_count, estimated_hours_per_carpenter,
|
|
total_work_hours, hourly_rate, total_labor_cost, work_breakdown, notes
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
carpenter_count = VALUES(carpenter_count),
|
|
estimated_hours_per_carpenter = VALUES(estimated_hours_per_carpenter),
|
|
total_work_hours = VALUES(total_work_hours),
|
|
hourly_rate = VALUES(hourly_rate),
|
|
total_labor_cost = VALUES(total_labor_cost),
|
|
work_breakdown = VALUES(work_breakdown),
|
|
notes = VALUES(notes),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
`;
|
|
|
|
const [result] = await this.db.pool.execute(query, [
|
|
projectId,
|
|
carpenterCount,
|
|
hoursPerCarpenter,
|
|
totalWorkHours,
|
|
this.HOURLY_RATE,
|
|
totalLaborCost,
|
|
JSON.stringify(workBreakdown),
|
|
notes
|
|
]);
|
|
|
|
logger.info('Project labor saved', {
|
|
projectId,
|
|
carpenterCount,
|
|
totalWorkHours,
|
|
totalLaborCost
|
|
});
|
|
|
|
return {
|
|
id: result.insertId || result.affectedRows,
|
|
carpenterCount,
|
|
hoursPerCarpenter,
|
|
totalWorkHours,
|
|
hourlyRate: this.HOURLY_RATE,
|
|
totalLaborCost,
|
|
workBreakdown
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error saving project labor:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Hent arbejdstimer data
|
|
async getProjectLabor(projectId) {
|
|
try {
|
|
const [rows] = await this.db.pool.execute(
|
|
'SELECT * FROM project_labor WHERE project_id = ?',
|
|
[projectId]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const labor = rows[0];
|
|
|
|
// Parse work breakdown JSON
|
|
let workBreakdown = [];
|
|
if (labor.work_breakdown) {
|
|
try {
|
|
workBreakdown = JSON.parse(labor.work_breakdown);
|
|
} catch (e) {
|
|
logger.warn('Failed to parse work breakdown JSON:', e);
|
|
}
|
|
}
|
|
|
|
return {
|
|
...labor,
|
|
work_breakdown: workBreakdown
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error getting project labor:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Estimer timer baseret på geometri (helper metode)
|
|
async estimateLaborFromGeometry(projectId) {
|
|
try {
|
|
const [geometryRows] = await this.db.pool.execute(
|
|
'SELECT * FROM roof_geometry WHERE project_id = ?',
|
|
[projectId]
|
|
);
|
|
|
|
if (geometryRows.length === 0) {
|
|
throw new Error('No geometry data found for project');
|
|
}
|
|
|
|
const geometry = geometryRows[0];
|
|
const workDistribution = this.calculateWorkDistribution(
|
|
geometry.estimated_work_hours,
|
|
geometry.estimated_carpenters
|
|
);
|
|
|
|
return {
|
|
suggestedCarpenterCount: geometry.estimated_carpenters,
|
|
suggestedTotalHours: geometry.estimated_work_hours,
|
|
workDistribution,
|
|
complexity: geometry.complexity_factor,
|
|
roofType: geometry.roof_type
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error estimating labor from geometry:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Genberegn omkostninger hvis timer ændres
|
|
async recalculateLaborCosts(projectId) {
|
|
try {
|
|
const labor = await this.getProjectLabor(projectId);
|
|
if (!labor) {
|
|
throw new Error('No labor data found for project');
|
|
}
|
|
|
|
const newTotalCost = labor.total_work_hours * this.HOURLY_RATE;
|
|
const newHoursPerCarpenter = this.calculateHoursPerCarpenter(
|
|
labor.total_work_hours,
|
|
labor.carpenter_count
|
|
);
|
|
|
|
const query = `
|
|
UPDATE project_labor
|
|
SET hourly_rate = ?, total_labor_cost = ?, estimated_hours_per_carpenter = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE project_id = ?
|
|
`;
|
|
|
|
await this.db.pool.execute(query, [
|
|
this.HOURLY_RATE,
|
|
newTotalCost,
|
|
newHoursPerCarpenter,
|
|
projectId
|
|
]);
|
|
|
|
logger.info('Labor costs recalculated', {
|
|
projectId,
|
|
newTotalCost,
|
|
hourlyRate: this.HOURLY_RATE
|
|
});
|
|
|
|
return {
|
|
totalLaborCost: newTotalCost,
|
|
hourlyRate: this.HOURLY_RATE,
|
|
hoursPerCarpenter: newHoursPerCarpenter
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error recalculating labor costs:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = ProjectLaborService;
|