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