Files
tilbudgivern/backend/src/services/projectLaborService.js
T
alex a02a96667a feat: Implement roof geometry service with complexity calculation, work hour estimation, and carpenter recommendations
- 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.
2025-09-15 23:05:56 +02:00

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;