feat: Enhance auto-save functionality by tracking user interaction in EnhancedGeometry component
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
<label>Tagtype:</label>
|
||||
<select
|
||||
value={geometryInput.roofType}
|
||||
onChange={(e) => setGeometryInput({...geometryInput, roofType: e.target.value})}
|
||||
onChange={(e) => {
|
||||
setHasUserInteracted(true);
|
||||
setIsSaved(false);
|
||||
setGeometryInput({...geometryInput, roofType: e.target.value});
|
||||
}}
|
||||
>
|
||||
<option value="sadeltag">Sadeltag/Skråttag</option>
|
||||
<option value="valmtag">Valmtag</option>
|
||||
@@ -1010,7 +1021,8 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
|
||||
onChange={(e) => {
|
||||
const newWidth = parseFloat(e.target.value) || 0;
|
||||
|
||||
// Mark as not saved when changing
|
||||
// Mark as user has interacted and not saved when changing
|
||||
setHasUserInteracted(true);
|
||||
setIsSaved(false);
|
||||
|
||||
// Recalculate ridge height if pitch is set
|
||||
@@ -1056,7 +1068,8 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
|
||||
onChange={(e) => {
|
||||
const newLength = parseFloat(e.target.value) || 0;
|
||||
|
||||
// Mark as not saved when changing
|
||||
// Mark as user has interacted and not saved when changing
|
||||
setHasUserInteracted(true);
|
||||
setIsSaved(false);
|
||||
|
||||
// Recalculate roof covering if we have width and ridge height
|
||||
@@ -1171,7 +1184,8 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
|
||||
const value = e.target.value;
|
||||
const newPitch = value === '' ? '' : parseFloat(value) || '';
|
||||
|
||||
// Mark as not saved when changing
|
||||
// Mark as user has interacted and not saved when changing
|
||||
setHasUserInteracted(true);
|
||||
setIsSaved(false);
|
||||
|
||||
// Auto-calculate ridge height from pitch and width
|
||||
@@ -1210,7 +1224,8 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
|
||||
const value = e.target.value;
|
||||
const newRidgeHeight = value === '' ? 0 : parseFloat(value) || 0;
|
||||
|
||||
// Mark as not saved when changing
|
||||
// Mark as user has interacted and not saved when changing
|
||||
setHasUserInteracted(true);
|
||||
setIsSaved(false);
|
||||
|
||||
// Auto-calculate pitch from ridge height and width
|
||||
@@ -1250,6 +1265,7 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
|
||||
value={geometryInput.wallHeight || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setHasUserInteracted(true);
|
||||
setIsSaved(false);
|
||||
setGeometryInput({
|
||||
...geometryInput,
|
||||
@@ -1278,7 +1294,8 @@ const EnhancedGeometry = ({ apiBaseUrl, project, selectedPackages, onGeometryCal
|
||||
const value = e.target.value;
|
||||
const newRoofCovering = value === '' ? 0 : parseFloat(value) || 0;
|
||||
|
||||
// Mark as not saved when changing
|
||||
// Mark as user has interacted and not saved when changing
|
||||
setHasUserInteracted(true);
|
||||
setIsSaved(false);
|
||||
|
||||
// Auto-calculate ridge height from tagbeklædning
|
||||
|
||||
Reference in New Issue
Block a user