feat: Enhance auto-save functionality by tracking user interaction in EnhancedGeometry component

This commit is contained in:
alexpolo1
2025-11-14 22:08:02 +00:00
parent a69e3850aa
commit 74affaca24
3 changed files with 80 additions and 30 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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