Files
tilbudgivern/unified-server.js
T
alex 185eb7664a fix: resolve React frontend path issue causing white screen
- Problem: __dirname resolved to /data/home/alex/git/tilbudgivern in PM2
- Solution: Use absolute paths for frontend build serving
- Changed from path.join(__dirname, 'frontend/build') to absolute path
- Now serving from /home/alex/git/tilbudgivern/frontend/build
- JavaScript and CSS files load correctly with HTTP 200 status
- Fixes white screen issue in all tabs including tømrer and materiale priser
2025-09-14 22:11:07 +02:00

879 lines
23 KiB
JavaScript

const express = require('express');
const path = require('path');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 4030;
// Load environment variables
require('dotenv').config();
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
// Configure multer for file uploads
const multer = require('multer');
const upload = multer({
dest: 'backend/uploads/',
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
},
fileFilter: (req, file, cb) => {
// Accept PDF files only
if (file.mimetype === 'application/pdf') {
cb(null, true);
} else {
cb(new Error('Only PDF files are allowed'), false);
}
}
});
// Initialize backend services
let openaiService, databaseService;
const initializeBackend = async () => {
try {
// Import backend services (both are singletons)
databaseService = require('./backend/src/services/databaseService');
openaiService = require('./backend/src/services/openaiService');
// Initialize database
await databaseService.initialize();
console.log('Database initialized successfully');
return true;
} catch (error) {
console.error('Failed to initialize backend:', error);
return false;
}
};
// API Routes - simplified versions of main endpoints
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
service: 'tilbudgivern-unified'
});
});
// OpenAI Stats endpoint
app.get('/api/quotes/openai/stats', async (req, res) => {
try {
if (!openaiService) {
return res.status(503).json({
success: false,
error: 'OpenAI service not initialized'
});
}
const stats = await openaiService.getTokenUsageStats();
res.json({
success: true,
tokenUsage: stats,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Error getting OpenAI stats:', error);
res.status(500).json({
success: false,
error: 'Kunne ikke hente token statistik',
timestamp: new Date().toISOString()
});
}
});
// OpenAI Stats endpoint - update button functionality
app.post('/api/quotes/openai/stats/update', async (req, res) => {
try {
if (!openaiService) {
return res.status(503).json({
success: false,
error: 'OpenAI service not initialized'
});
}
// Force refresh actual usage data from OpenAI API
const actualUsage = await openaiService.fetchActualUsageFromAPI();
// Get updated stats
const stats = await openaiService.getTokenUsageStats();
res.json({
success: true,
message: 'OpenAI budget data updated',
tokenUsage: stats,
actualUsage: actualUsage,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Error updating OpenAI stats:', error);
res.status(500).json({
success: false,
error: 'Kunne ikke opdatere budget data',
details: error.message,
timestamp: new Date().toISOString()
});
}
});
// Quote generation endpoint
app.post('/api/quotes/generate', async (req, res) => {
try {
if (!openaiService || !databaseService) {
return res.status(503).json({
success: false,
error: 'Services not initialized'
});
}
const { description, area, projectType, additionalInfo, customerEmail } = req.body;
// Basic validation
if (!description || description.length < 10) {
return res.status(400).json({
success: false,
error: 'Beskrivelse skal være mindst 10 tegn'
});
}
console.log('Generating quote', { description, area, projectType });
// Generate quote using OpenAI
const result = await openaiService.generateQuote({
description,
area: parseInt(area) || null,
projectType,
additionalInfo,
region: 'DK'
});
// Save quote to database
const savedQuote = await databaseService.saveQuote({
customerEmail,
description,
area,
projectType,
quote: result.quote,
metadata: result.metadata
});
res.json({
success: true,
quoteId: savedQuote.id,
quote: result.quote,
metadata: result.metadata
});
} catch (error) {
console.error('Error generating quote:', error);
res.status(500).json({
success: false,
error: error.message || 'Der opstod en fejl ved generering af tilbud'
});
}
});
// Carpenter hours calculation endpoint
app.post('/api/quotes/carpenter-hours', async (req, res) => {
try {
if (!openaiService) {
return res.status(500).json({
success: false,
error: 'OpenAI service ikke tilgængelig'
});
}
const { project_description, project_area, project_type } = req.body;
// Validate input
if (!project_description || !project_type) {
return res.status(400).json({
success: false,
error: 'Projekt beskrivelse og type er påkrævet'
});
}
// Calculate carpenter hours using OpenAI
const calculation = await openaiService.calculateCarpenterHours(
project_description,
project_area,
project_type
);
res.json({
success: true,
calculation: calculation
});
} catch (error) {
console.error('Error calculating carpenter hours:', error);
res.status(500).json({
success: false,
error: error.message || 'Der opstod en fejl ved beregning af tømrer timer'
});
}
});
// Save carpenter calculation to database
app.post('/api/quotes/save-carpenter-calculation', async (req, res) => {
try {
if (!databaseService) {
return res.status(500).json({
success: false,
error: 'Database service ikke tilgængelig'
});
}
const { calculation } = req.body;
if (!calculation) {
return res.status(400).json({
success: false,
error: 'Beregnings data er påkrævet'
});
}
// Save calculation to database
const result = await databaseService.saveCarpenterCalculation(calculation);
res.json({
success: true,
saved_calculation: {
id: result.id,
created_at: result.created_at
}
});
} catch (error) {
console.error('Error saving carpenter calculation:', error);
res.status(500).json({
success: false,
error: error.message || 'Der opstod en fejl ved gemning af beregning'
});
}
});
// Auth login endpoint
app.post('/api/auth/login', (req, res) => {
try {
const { username, password } = req.body;
// Simple hardcoded credentials - same as backend
const VALID_CREDENTIALS = {
username: process.env.AUTH_USERNAME || 'toemrer',
password: process.env.AUTH_PASSWORD || 'REDACTED_AUTH'
};
// Basic validation
if (!username || !password) {
return res.status(400).json({
success: false,
error: 'Ugyldig anmodning'
});
}
// Check credentials
if (username === VALID_CREDENTIALS.username && password === VALID_CREDENTIALS.password) {
console.log('Successful login attempt:', { username, ip: req.ip });
res.json({
success: true,
message: 'Login successful'
});
} else {
console.log('Failed login attempt:', { username, ip: req.ip });
res.status(401).json({
success: false,
error: 'Forkert brugernavn eller adgangskode'
});
}
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
error: 'Der opstod en fejl'
});
}
});
// Legacy auth endpoint
app.post('/api/auth', (req, res) => {
res.json({
success: true,
message: 'Basic auth endpoint',
timestamp: new Date().toISOString()
});
});
// Categories endpoints
app.get('/api/categories/list', async (req, res) => {
try {
const categories = await databaseService.getAllCategories();
res.json({
success: true,
materialCategories: categories.materialCategories,
projectTypes: categories.projectTypes
});
} catch (error) {
console.error('Error fetching categories:', error);
res.status(500).json({ success: false, message: 'Error fetching categories' });
}
});
// Get labor prices for category
app.get('/api/categories/labor/:category', async (req, res) => {
try {
const { category } = req.params;
const laborPrices = await databaseService.getLaborPricesByCategory(category);
res.json({
success: true,
laborPrices: laborPrices || []
});
} catch (error) {
console.error('Error fetching labor prices:', error);
res.status(500).json({
success: false,
error: 'Fejl ved hentning af arbejdspriser'
});
}
});
// Get material prices for category
app.get('/api/categories/materials/:category', async (req, res) => {
try {
const { category } = req.params;
const materialPrices = await databaseService.getMaterialPricesByCategory(category);
res.json({
success: true,
materialPrices: materialPrices || []
});
} catch (error) {
console.error('Error fetching material prices:', error);
res.status(500).json({
success: false,
error: 'Fejl ved hentning af materialepriser'
});
}
});
// Add new labor price
app.post('/api/categories/labor', async (req, res) => {
try {
const {
projectType,
description,
hourlyRate,
area,
totalHours,
difficulty,
notes,
location
} = req.body;
if (!projectType || !description || !hourlyRate) {
return res.status(400).json({
success: false,
error: 'Projekttype, beskrivelse og timepris er påkrævet'
});
}
// For now, just log the data instead of saving to database
console.log('New labor price submission:', {
projectType,
description,
hourlyRate: parseFloat(hourlyRate),
area: area ? parseFloat(area) : null,
totalHours: totalHours ? parseFloat(totalHours) : null,
difficulty: difficulty || 'normal',
location: location || null,
notes: notes || null
});
res.json({
success: true,
message: 'Arbejdspris tilføjet succesfuldt (demo mode)'
});
} catch (error) {
console.error('Error adding labor price:', error);
res.status(500).json({
success: false,
error: 'Fejl ved tilføjelse af arbejdspris'
});
}
});
// Add new material price
app.post('/api/categories/materials', async (req, res) => {
try {
const {
name,
category,
subcategory,
price,
unit,
supplierName,
sku,
description,
notes
} = req.body;
if (!name || !category || !price) {
return res.status(400).json({
success: false,
error: 'Navn, kategori og pris er påkrævet'
});
}
// For now, just log the data instead of saving to database
console.log('New material price submission:', {
name,
category,
subcategory: subcategory || null,
price: parseFloat(price),
unit: unit || 'stk',
supplierName: supplierName || null,
sku: sku || null,
description: description || null,
notes: notes || null
});
res.json({
success: true,
message: 'Materialepris tilføjet succesfuldt (demo mode)'
});
} catch (error) {
console.error('Error adding material price:', error);
res.status(500).json({
success: false,
error: 'Fejl ved tilføjelse af materialepris'
});
}
});
// Update labor price
// Create new labor price
app.post('/api/labor-price', async (req, res) => {
try {
const { project_type, description, labor_rate } = req.body;
const result = await databaseService.query(
'INSERT INTO project_quotes (project_type, notes, labor_rate, labor_cost, material_cost, overhead_cost, total_excl_vat, vat_amount, total_incl_vat, difficulty_multiplier) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[project_type, description, labor_rate, 0, 0, 0, 0, 0, 0, 1.0]
);
res.json({
success: true,
message: 'Labor price created successfully',
id: result.insertId
});
} catch (error) {
console.error('Error creating labor price:', error);
res.status(500).json({ success: false, message: 'Error creating labor price' });
}
});
// Update labor price
app.put('/api/labor-price/:id', async (req, res) => {
try {
const { id } = req.params;
const { description, labor_rate } = req.body;
const result = await databaseService.query(
'UPDATE project_quotes SET notes = ?, labor_rate = ? WHERE id = ?',
[description, labor_rate, id]
);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: 'Labor price not found' });
}
res.json({ success: true, message: 'Labor price updated successfully' });
} catch (error) {
console.error('Error updating labor price:', error);
res.status(500).json({ success: false, message: 'Error updating labor price' });
}
});
// Delete labor price
app.delete('/api/labor-price/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await databaseService.query(
'DELETE FROM project_quotes WHERE id = ?',
[id]
);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: 'Labor price not found' });
}
res.json({ success: true, message: 'Labor price deleted successfully' });
} catch (error) {
console.error('Error deleting labor price:', error);
res.status(500).json({ success: false, message: 'Error deleting labor price' });
}
});
// Delete labor price
app.delete('/api/categories/labor/:id', async (req, res) => {
try {
const { id } = req.params;
console.log('Delete labor price:', id);
res.json({
success: true,
message: 'Arbejdspris slettet succesfuldt (demo mode)'
});
} catch (error) {
console.error('Error deleting labor price:', error);
res.status(500).json({
success: false,
error: 'Fejl ved sletning af arbejdspris'
});
}
});
// Update material price
app.put('/api/categories/materials/:id', async (req, res) => {
try {
const { id } = req.params;
const {
name,
category,
subcategory,
price,
unit,
supplierName,
sku,
description,
notes
} = req.body;
if (!name || !category || !price) {
return res.status(400).json({
success: false,
error: 'Navn, kategori og pris er påkrævet'
});
}
console.log('Update material price:', id, {
name,
category,
subcategory: subcategory || null,
price: parseFloat(price),
unit: unit || 'stk',
supplierName: supplierName || null,
sku: sku || null,
description: description || null,
notes: notes || null
});
res.json({
success: true,
message: 'Materialepris opdateret succesfuldt (demo mode)'
});
} catch (error) {
console.error('Error updating material price:', error);
res.status(500).json({
success: false,
error: 'Fejl ved opdatering af materialepris'
});
}
});
// Delete material price
app.delete('/api/categories/materials/:id', async (req, res) => {
try {
const { id } = req.params;
console.log('Delete material price:', id);
res.json({
success: true,
message: 'Materialepris slettet succesfuldt (demo mode)'
});
} catch (error) {
console.error('Error deleting material price:', error);
res.status(500).json({
success: false,
error: 'Fejl ved sletning af materialepris'
});
}
});
// Main categories endpoint with counts
app.get('/api/categories', async (req, res) => {
try {
const categories = await databaseService.getAllCategories();
// Combine material categories and project types for dropdown
const allCategories = [
...categories.materialCategories.map(cat => `${cat.category} (${cat.count})`),
...categories.projectTypes.map(proj => `${proj.project_type} (${proj.count})`)
];
res.json({
success: true,
categories: allCategories,
materialCategories: categories.materialCategories,
projectTypes: categories.projectTypes
});
} catch (error) {
console.error('Error fetching categories:', error);
// Fallback to hardcoded list
res.json({
success: true,
categories: [
'Køkken',
'Badeværelse',
'Stue/Soveværelse',
'Udendørs/Terrasse',
'Kælder/Loft',
'Energioptimering',
'Gulve',
'Vinduer/Døre',
'Maleri/Tapetsering',
'Elektriker',
'VVS',
'Murerarbejde',
'Tømrerarbejde',
'Anlæg/Have',
'Diverse reparationer'
]
});
}
});
// File upload endpoint
app.post('/api/uploads', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: 'Ingen fil uploaded'
});
}
console.log('File uploaded:', req.file.filename);
res.json({
success: true,
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
message: 'Fil uploaded succesfuldt'
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({
success: false,
error: 'Der opstod en fejl ved upload af fil'
});
}
});
// Pricing endpoints for "Tømrer priser" and "Materiale priser"
app.get('/api/pricing/health', (req, res) => {
res.json({
success: true,
status: 'Pricing service er kørende',
timestamp: new Date().toISOString()
});
});
// Get historical material prices
app.get('/api/pricing/history/:materialName', async (req, res) => {
try {
const { materialName } = req.params;
const { limit = 10 } = req.query;
if (!databaseService) {
return res.status(503).json({
success: false,
error: 'Database service not available'
});
}
const historicalPrices = await databaseService.getHistoricalMaterialPrices(
materialName,
parseInt(limit)
);
res.json({
success: true,
materialName,
historicalPrices,
count: historicalPrices.length,
message: `Fundet ${historicalPrices.length} historiske priser for ${materialName}`
});
} catch (error) {
console.error('Error getting historical prices:', error);
res.status(500).json({
success: false,
error: 'Fejl ved hentning af historiske priser',
details: error.message
});
}
});
// Search materials
app.get('/api/pricing/search', async (req, res) => {
try {
const { q: searchTerm, type: projectType, limit = 20 } = req.query;
if (!searchTerm) {
return res.status(400).json({
success: false,
error: 'Søgeterm (q) er påkrævet'
});
}
if (!databaseService) {
return res.status(503).json({
success: false,
error: 'Database service not available'
});
}
const searchResults = await databaseService.searchOcrMaterials(
searchTerm,
projectType,
parseInt(limit)
);
res.json({
success: true,
searchTerm,
projectType: projectType || 'alle',
results: searchResults,
totalMatches: searchResults.length,
message: `Fundet ${searchResults.length} materialer for "${searchTerm}"`
});
} catch (error) {
console.error('Error searching materials:', error);
res.status(500).json({
success: false,
error: 'Fejl ved søgning i materialer',
details: error.message
});
}
});
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'tilbudgivern-unified',
timestamp: new Date().toISOString()
});
});
// Serve static files from React build
const frontendBuildPath = '/home/alex/git/tilbudgivern/frontend/build';
app.use(express.static(frontendBuildPath));
// Handle React Router - serve index.html for all non-API routes
app.get('*', (req, res) => {
const indexPath = '/home/alex/git/tilbudgivern/frontend/build/index.html';
res.sendFile(indexPath);
});
// Create new material price
app.post('/api/material-price', async (req, res) => {
try {
const { name, unit, unit_price, supplier_name, material_category } = req.body;
// Create a document entry first
const docResult = await databaseService.query(
'INSERT INTO ocr_documents (filename, supplier_name, document_type, processing_status) VALUES (?, ?, ?, ?)',
['Manual Entry', supplier_name || 'Manual', 'price_list', 'processed']
);
// Then create the material entry
const result = await databaseService.query(
'INSERT INTO ocr_materials (ocr_document_id, name, unit, unit_price, total_price, material_category, quantity, confidence) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[docResult.insertId, name, unit, unit_price, unit_price, material_category, 1, 1.0]
);
res.json({
success: true,
message: 'Material price created successfully',
id: result.insertId
});
} catch (error) {
console.error('Error creating material price:', error);
res.status(500).json({ success: false, message: 'Error creating material price' });
}
});
// Update material price
app.put('/api/material-price/:id', async (req, res) => {
try {
const { id } = req.params;
const { name, unit, unit_price, material_category } = req.body;
const result = await databaseService.query(
'UPDATE ocr_materials SET name = ?, unit = ?, unit_price = ?, total_price = ?, material_category = ? WHERE id = ?',
[name, unit, unit_price, unit_price, material_category, id]
);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: 'Material price not found' });
}
res.json({ success: true, message: 'Material price updated successfully' });
} catch (error) {
console.error('Error updating material price:', error);
res.status(500).json({ success: false, message: 'Error updating material price' });
}
});
// Delete material price
app.delete('/api/material-price/:id', async (req, res) => {
try {
const { id } = req.params;
const result = await databaseService.query(
'DELETE FROM ocr_materials WHERE id = ?',
[id]
);
if (result.affectedRows === 0) {
return res.status(404).json({ success: false, message: 'Material price not found' });
}
res.json({ success: true, message: 'Material price deleted successfully' });
} catch (error) {
console.error('Error deleting material price:', error);
res.status(500).json({ success: false, message: 'Error deleting material price' });
}
});
// Start server
const startServer = async () => {
const backendReady = await initializeBackend();
app.listen(PORT, () => {
console.log(`Unified server running on port ${PORT}`);
console.log('Frontend: Serving React app');
console.log('Backend: API available at /api/*');
console.log(`Backend services: ${backendReady ? 'Ready' : 'Failed'}`);
});
};
startServer();