- 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
879 lines
23 KiB
JavaScript
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();
|