diff --git a/backend/src/services/ordrestyringSyncService.js b/backend/src/services/ordrestyringSyncService.js
index 1cb128e..2dbd8f4 100644
--- a/backend/src/services/ordrestyringSyncService.js
+++ b/backend/src/services/ordrestyringSyncService.js
@@ -283,7 +283,15 @@ class OrdrestyringSyncService {
} catch (error) {
// Handle API errors gracefully - don't crash sync
if (error.response?.status === 500) {
- logger.warn('Ordrestyring API /hours endpoint returned 500 - skipping hours sync');
+ logger.info('hours sync completed: no changes', {
+ note: 'Ordrestyring API /hours endpoint temporarily unavailable (500) - skipped'
+ });
+ return { hasChanges: false, changes: 0, skipped: true };
+ }
+ if (error.response?.status === 404) {
+ logger.info('hours sync completed: no changes', {
+ note: 'Ordrestyring API /hours endpoint not found (404) - skipped'
+ });
return { hasChanges: false, changes: 0, skipped: true };
}
logger.error('Error syncing hours:', error.message);
diff --git a/backend/unified-server.js b/backend/unified-server.js
index 50b154a..c7e0d77 100644
--- a/backend/unified-server.js
+++ b/backend/unified-server.js
@@ -193,8 +193,9 @@ app.use('/api', sessionErrorLogger);
// Request logging middleware (log all API calls)
app.use('/api', requestLogger);
-// Response error logger (capture error responses)
-app.use('/api', responseErrorLogger);
+// Response error logger (capture error responses) - DISABLED due to hanging issue
+// app.use('/api', responseErrorLogger);
+
// Material quantity calculation with formulas
async function calculateMaterialQuantities(projectId, geometry) {
@@ -6915,9 +6916,15 @@ app.get('/api/customer-projects/:projectId/calculation', async (req, res) => {
// Save roof geometry
app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
+ // Declare projectId outside try block so it's available in error handler
+ let projectId = null;
+ let standardRoofWidth = null;
+ let standardRoofLength = null;
+ let roofType = null;
+
try {
const { projectId: rawProjectId } = req.params;
- const projectId = parseInt(rawProjectId, 10);
+ projectId = parseInt(rawProjectId, 10);
console.log('🔍 Geometry save request received:', {
rawProjectId: rawProjectId,
@@ -6938,19 +6945,37 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
// Accept both field name formats for compatibility
const {
roofWidth, roofLength, length_main, width_main,
- roofPitch, roofType, complexity, wallHeight, ridgeHeight, notes,
+ roofPitch, pitch, roofType: frontendRoofType, complexity, wallHeight, ridgeHeight, notes,
kvistSize, numberOfKviste, roofCoveringArea,
isCornerHouse, wing2Width, wing2Length
} = req.body;
// Standardize field names - accept both old and new formats
- const standardRoofWidth = roofWidth || width_main;
- const standardRoofLength = roofLength || length_main;
+ standardRoofWidth = roofWidth || width_main;
+ standardRoofLength = roofLength || length_main;
+ const standardRoofPitch = roofPitch || pitch; // Accept both 'roofPitch' and 'pitch'
+
+ // Map frontend roof type values to database ENUM values
+ const roofTypeMapping = {
+ 'sadeltag': 'skraat_tag',
+ 'valmtag': 'skraat_tag',
+ 'koebenhavnertag': 'skraat_tag',
+ 'pulttag': 'skraat_tag',
+ 'tag_med_kviste': 'komplekst',
+ 'fladt_tag': 'fladt_tag',
+ 'mansard': 'mansard'
+ };
+
+ roofType = roofTypeMapping[frontendRoofType] || frontendRoofType || 'skraat_tag';
+
+ if (frontendRoofType && frontendRoofType !== roofType) {
+ console.log(`🔄 Mapped roof type: "${frontendRoofType}" → "${roofType}"`);
+ }
console.log('🔍 Validating geometry data:', {
roofWidth: standardRoofWidth,
roofLength: standardRoofLength,
- roofPitch: roofPitch,
+ roofPitch: standardRoofPitch,
roofType: roofType,
complexity: complexity,
wallHeight: wallHeight,
@@ -6958,7 +6983,8 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
notes: notes,
fieldMapping: {
width: `${roofWidth ? 'roofWidth' : 'width_main'} -> ${standardRoofWidth}`,
- length: `${roofLength ? 'roofLength' : 'length_main'} -> ${standardRoofLength}`
+ length: `${roofLength ? 'roofLength' : 'length_main'} -> ${standardRoofLength}`,
+ pitch: `${roofPitch ? 'roofPitch' : 'pitch'} -> ${standardRoofPitch}`
}
});
@@ -6979,7 +7005,7 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
const width = parseFloat(standardRoofWidth);
const length = parseFloat(standardRoofLength);
- const pitch = parseFloat(roofPitch);
+ const pitchValue = parseFloat(standardRoofPitch);
// Realistic size constraints for residential/commercial buildings
if (isNaN(width) || width < 2.5 || width > 50) {
@@ -7035,8 +7061,8 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
// Roof type specific validation
if (roofType === 'skraat_tag') {
// Expanded range: 15-80° allows for standard roofs (15-50°) AND steep designs (A-frame, etc.)
- if (isNaN(pitch) || pitch < 15 || pitch > 80) {
- console.log('❌ Validation error: Invalid pitch for pitched roof:', pitch);
+ if (isNaN(pitchValue) || pitchValue < 15 || pitchValue > 80) {
+ console.log('❌ Validation error: Invalid pitch for pitched roof:', pitchValue);
return res.status(400).json({
success: false,
error: 'Taghældning skal være mellem 15° og 80° (15-45° standard, 45-80° stejle tage som A-frame)'
@@ -7044,7 +7070,7 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
}
// Check realistic proportions for pitched roof (increased to 20m to allow steep roofs)
- const roofHeight = Math.tan(pitch * Math.PI / 180) * (width / 2);
+ const roofHeight = Math.tan(pitchValue * Math.PI / 180) * (width / 2);
if (roofHeight > 20) {
console.log('❌ Validation error: Unrealistic roof height:', roofHeight);
return res.status(400).json({
@@ -7134,16 +7160,8 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
console.log('✅ Project found:', projectExists[0].project_name);
- // Validate roof_type against ENUM values
- const validRoofTypes = ['fladt_tag', 'skraat_tag', 'mansard', 'komplekst', 'sadeltag', 'valmtag', 'koebenhavnertag', 'pulttag', 'tag_med_kviste'];
- if (roofType && !validRoofTypes.includes(roofType)) {
- console.log('❌ Invalid roof type:', roofType);
- return res.status(400).json({
- success: false,
- error: `Ugyldig tagtype: "${roofType}". Gyldige typer: ${validRoofTypes.join(', ')}`,
- technical_details: `roof_type '${roofType}' not in ENUM(${validRoofTypes.join(', ')})`
- });
- }
+ // roofType is already mapped to database ENUM values, no need to validate
+ console.log('📋 Using mapped roof type:', roofType);
// Calculate area based on Bolius.dk methodology
// For pitched roofs (sadeltag), use: length × height × 2
@@ -7320,14 +7338,18 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
}
// Fetch the saved/updated geometry to return accurate data
+ console.log('📥 Fetching saved geometry for project:', projectId);
const savedGeometry = await databaseService.query(
'SELECT * FROM roof_geometry WHERE project_id = ? ORDER BY updated_at DESC LIMIT 1',
[projectId]
);
+ console.log('📥 Fetched geometry:', savedGeometry);
const geometry = savedGeometry[0];
+ console.log('📦 Using geometry object:', geometry);
// Broadcast real-time update to connected clients
+ console.log('📡 Broadcasting update for project:', projectId);
broadcastProjectUpdate(projectId, 'geometry_updated', {
geometryId: geometry.id,
area: area,
@@ -7336,7 +7358,9 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
vindskede_lbm: vindskede_lbm,
materialQuantities: materialQuantities
});
+ console.log('✅ Broadcast complete');
+ console.log('📤 Sending response...');
res.json({
success: true,
geometryId: geometry.id,
@@ -7347,6 +7371,7 @@ app.post('/api/customer-projects/:projectId/geometry', async (req, res) => {
geometry: geometry, // Return full geometry object
message: 'Geometri gemt succesfuldt med Bolius-baseret beregning'
});
+ console.log('✅ Response sent successfully');
} catch (error) {
logError('GEOMETRY_SAVE', error, {
projectId: projectId,
diff --git a/frontend/src/components/EnhancedGeometry.js b/frontend/src/components/EnhancedGeometry.js
index 8a000f1..4e5cf8d 100644
--- a/frontend/src/components/EnhancedGeometry.js
+++ b/frontend/src/components/EnhancedGeometry.js
@@ -380,6 +380,7 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
const [isAutoSaving, setIsAutoSaving] = useState(false);
const [autoSaveCountdown, setAutoSaveCountdown] = useState(3); // Countdown from 3 to 0
const [isSaved, setIsSaved] = useState(false); // Track if current data is saved
+ const [hasUserInteracted, setHasUserInteracted] = useState(false); // Track if user has changed any input
// Auto-save will be defined after handleGeometryCalculation
@@ -807,6 +808,12 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
// Auto-save effect - triggers 3 seconds after input changes
React.useEffect(() => {
+ // Don't auto-save if user hasn't interacted yet (e.g., initial load)
+ if (!hasUserInteracted) {
+ console.log('⏸️ Skipping auto-save - user has not interacted yet');
+ return;
+ }
+
console.log('🔄 Geometry input changed, starting auto-save countdown...', {
width: geometryInput.width,
length: geometryInput.length,
@@ -842,7 +849,7 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
if (timeout) clearTimeout(timeout);
if (countdownInterval) clearInterval(countdownInterval);
};
- }, [geometryInput, autoSaveGeometry]);
+ }, [geometryInput, autoSaveGeometry, hasUserInteracted]);
// Live SVG update effect - opdaterer SVG når input ændres
React.useEffect(() => {
@@ -975,7 +982,11 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal