Projektsuche Implementiert mit anbindung an INFRA
This commit is contained in:
25
database.js
25
database.js
@@ -336,6 +336,31 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MSSQL-Konfigurations-Tabelle für externe Projekt-/Kunden-Datenbank
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS mssql_config (
|
||||||
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||||
|
server TEXT,
|
||||||
|
database TEXT,
|
||||||
|
username TEXT,
|
||||||
|
password TEXT,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CHECK (id = 1)
|
||||||
|
)`, (err) => {
|
||||||
|
if (err && !err.message.includes('duplicate column')) {
|
||||||
|
console.warn('Warnung beim Erstellen der mssql_config Tabelle:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Standard-Eintrag für mssql_config erstellen falls nicht vorhanden
|
||||||
|
db.run(
|
||||||
|
`INSERT OR IGNORE INTO mssql_config (id, server, database, username, password) VALUES (1, '', '', '', '')`,
|
||||||
|
(err) => {
|
||||||
|
if (err && !err.message.includes('UNIQUE constraint')) {
|
||||||
|
console.warn('Warnung beim Erstellen des Standard-Eintrags für mssql_config:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Migration: Bestehende Rollen zu JSON-Arrays konvertieren
|
// Migration: Bestehende Rollen zu JSON-Arrays konvertieren
|
||||||
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
|
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
|
||||||
db.all('SELECT id, role FROM users', (err, users) => {
|
db.all('SELECT id, role FROM users', (err, users) => {
|
||||||
|
|||||||
1340
package-lock.json
generated
1340
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"ldapjs": "^3.0.7",
|
"ldapjs": "^3.0.7",
|
||||||
|
"mssql": "^11.0.0",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"pdfkit": "^0.13.0",
|
"pdfkit": "^0.13.0",
|
||||||
"ping": "^0.4.4",
|
"ping": "^0.4.4",
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Optionen laden
|
// Optionen laden
|
||||||
loadOptions();
|
loadOptions();
|
||||||
|
|
||||||
|
// MSSQL-Konfiguration laden
|
||||||
|
loadMssqlConfig();
|
||||||
|
|
||||||
// Optionen-Formular
|
// Optionen-Formular
|
||||||
const optionsForm = document.getElementById('optionsForm');
|
const optionsForm = document.getElementById('optionsForm');
|
||||||
if (optionsForm) {
|
if (optionsForm) {
|
||||||
@@ -151,6 +154,96 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MSSQL-Konfigurationsformular
|
||||||
|
const mssqlConfigForm = document.getElementById('mssqlConfigForm');
|
||||||
|
if (mssqlConfigForm) {
|
||||||
|
mssqlConfigForm.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const server = document.getElementById('mssqlServer').value;
|
||||||
|
const database = document.getElementById('mssqlDatabase').value;
|
||||||
|
const username = document.getElementById('mssqlUsername').value;
|
||||||
|
const password = document.getElementById('mssqlPassword').value;
|
||||||
|
|
||||||
|
if (!server || !database || !username) {
|
||||||
|
alert('Bitte Server, Datenbankname und Benutzername ausfüllen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = { server, database, username, password };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/admin/mssql-config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
alert('MSSQL-Konfiguration wurde erfolgreich gespeichert!');
|
||||||
|
// Passwort-Feld leeren nach dem Speichern
|
||||||
|
document.getElementById('mssqlPassword').value = '';
|
||||||
|
} else {
|
||||||
|
alert('Fehler: ' + (result.error || 'MSSQL-Konfiguration konnte nicht gespeichert werden'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler:', error);
|
||||||
|
alert('Fehler beim Speichern der MSSQL-Konfiguration');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSSQL Test-Verbindung
|
||||||
|
const mssqlTestBtn = document.getElementById('mssqlTestConnectionBtn');
|
||||||
|
if (mssqlTestBtn) {
|
||||||
|
mssqlTestBtn.addEventListener('click', async function() {
|
||||||
|
const statusEl = document.getElementById('mssqlTestStatus');
|
||||||
|
mssqlTestBtn.disabled = true;
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = 'Verbindung wird getestet...';
|
||||||
|
statusEl.style.color = 'blue';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/admin/mssql-test-connection', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result && result.success) {
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = 'Verbindung erfolgreich.';
|
||||||
|
statusEl.style.color = 'green';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const msg = (result && result.error) ? result.error : 'Testverbindung fehlgeschlagen.';
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = msg;
|
||||||
|
statusEl.style.color = 'red';
|
||||||
|
}
|
||||||
|
alert(msg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei MSSQL-Testverbindung:', error);
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = 'Fehler bei der Testverbindung.';
|
||||||
|
statusEl.style.color = 'red';
|
||||||
|
}
|
||||||
|
alert('Fehler bei der Testverbindung zur MSSQL-Datenbank.');
|
||||||
|
} finally {
|
||||||
|
mssqlTestBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Sync-Button
|
// Sync-Button
|
||||||
const syncNowBtn = document.getElementById('syncNowBtn');
|
const syncNowBtn = document.getElementById('syncNowBtn');
|
||||||
if (syncNowBtn) {
|
if (syncNowBtn) {
|
||||||
@@ -271,6 +364,31 @@ async function loadLDAPConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MSSQL-Konfiguration laden und Formular ausfüllen
|
||||||
|
async function loadMssqlConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/admin/mssql-config');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.config) {
|
||||||
|
const config = result.config;
|
||||||
|
|
||||||
|
if (document.getElementById('mssqlServer')) {
|
||||||
|
document.getElementById('mssqlServer').value = config.server || '';
|
||||||
|
}
|
||||||
|
if (document.getElementById('mssqlDatabase')) {
|
||||||
|
document.getElementById('mssqlDatabase').value = config.database || '';
|
||||||
|
}
|
||||||
|
if (document.getElementById('mssqlUsername')) {
|
||||||
|
document.getElementById('mssqlUsername').value = config.username || '';
|
||||||
|
}
|
||||||
|
// Passwort wird aus Sicherheitsgründen nie vorausgefüllt
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der MSSQL-Konfiguration:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteUser(userId, username) {
|
async function deleteUser(userId, username) {
|
||||||
const confirmed = confirm(`Möchten Sie den Benutzer "${username}" wirklich löschen?`);
|
const confirmed = confirm(`Möchten Sie den Benutzer "${username}" wirklich löschen?`);
|
||||||
|
|
||||||
|
|||||||
@@ -3,47 +3,53 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const { db } = require('../database');
|
const { db } = require('../database');
|
||||||
const { requireAdmin } = require('../middleware/auth');
|
const { requireAdmin } = require('../middleware/auth');
|
||||||
|
const { testMssqlConnection } = require('../services/mssql-infra-service');
|
||||||
|
|
||||||
// Routes registrieren
|
// Routes registrieren
|
||||||
function registerAdminRoutes(app) {
|
function registerAdminRoutes(app) {
|
||||||
// Admin-Bereich
|
// Admin-Bereich
|
||||||
app.get('/admin', requireAdmin, (req, res) => {
|
app.get('/admin', requireAdmin, (req, res) => {
|
||||||
db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage, default_break_minutes, created_at FROM users ORDER BY created_at DESC',
|
db.all(
|
||||||
|
'SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage, default_break_minutes, created_at FROM users ORDER BY created_at DESC',
|
||||||
(err, users) => {
|
(err, users) => {
|
||||||
// LDAP-Konfiguration, Sync-Log und Optionen abrufen
|
// LDAP-Konfiguration, Sync-Log, Optionen und MSSQL-Konfiguration abrufen
|
||||||
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => {
|
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => {
|
||||||
db.all('SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT 10', (err, syncLogs) => {
|
db.all('SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT 10', (err, syncLogs) => {
|
||||||
db.get('SELECT * FROM system_options WHERE id = 1', (err, options) => {
|
db.get('SELECT * FROM system_options WHERE id = 1', (err, options) => {
|
||||||
// Parse Rollen für jeden User
|
db.get('SELECT * FROM mssql_config WHERE id = 1', (err, mssqlConfig) => {
|
||||||
const usersWithRoles = (users || []).map(u => {
|
// Parse Rollen für jeden User
|
||||||
let roles = [];
|
const usersWithRoles = (users || []).map(u => {
|
||||||
try {
|
let roles = [];
|
||||||
roles = JSON.parse(u.role);
|
try {
|
||||||
if (!Array.isArray(roles)) {
|
roles = JSON.parse(u.role);
|
||||||
roles = [u.role];
|
if (!Array.isArray(roles)) {
|
||||||
|
roles = [u.role];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
roles = [u.role || 'mitarbeiter'];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return { ...u, roles };
|
||||||
roles = [u.role || 'mitarbeiter'];
|
});
|
||||||
}
|
|
||||||
return { ...u, roles };
|
|
||||||
});
|
|
||||||
|
|
||||||
res.render('admin', {
|
res.render('admin', {
|
||||||
users: usersWithRoles,
|
users: usersWithRoles,
|
||||||
ldapConfig: ldapConfig || null,
|
ldapConfig: ldapConfig || null,
|
||||||
syncLogs: syncLogs || [],
|
syncLogs: syncLogs || [],
|
||||||
options: options || { saturday_percentage: 100, sunday_percentage: 100 },
|
options: options || { saturday_percentage: 100, sunday_percentage: 100 },
|
||||||
user: {
|
mssqlConfig: mssqlConfig || null,
|
||||||
firstname: req.session.firstname,
|
user: {
|
||||||
lastname: req.session.lastname,
|
firstname: req.session.firstname,
|
||||||
roles: req.session.roles || [],
|
lastname: req.session.lastname,
|
||||||
currentRole: req.session.currentRole || 'admin'
|
roles: req.session.roles || [],
|
||||||
}
|
currentRole: req.session.currentRole || 'admin'
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Benutzer erstellen
|
// Benutzer erstellen
|
||||||
@@ -234,6 +240,88 @@ function registerAdminRoutes(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MSSQL-Konfiguration laden
|
||||||
|
app.get('/admin/mssql-config', requireAdmin, (req, res) => {
|
||||||
|
db.get('SELECT server, database, username FROM mssql_config WHERE id = 1', (err, config) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Laden der MSSQL-Konfiguration' });
|
||||||
|
}
|
||||||
|
if (!config) {
|
||||||
|
return res.json({
|
||||||
|
config: {
|
||||||
|
server: '',
|
||||||
|
database: '',
|
||||||
|
username: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json({ config });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// MSSQL-Konfiguration speichern
|
||||||
|
app.post('/admin/mssql-config', requireAdmin, (req, res) => {
|
||||||
|
const { server, database, username, password } = req.body;
|
||||||
|
|
||||||
|
const trimmedServer = server ? server.trim() : '';
|
||||||
|
const trimmedDatabase = database ? database.trim() : '';
|
||||||
|
const trimmedUsername = username ? username.trim() : '';
|
||||||
|
const trimmedPassword = password != null ? password.trim() : null;
|
||||||
|
|
||||||
|
if (!trimmedServer || !trimmedDatabase || !trimmedUsername) {
|
||||||
|
return res.status(400).json({ error: 'Server, Datenbankname und Benutzername sind erforderlich' });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.get('SELECT * FROM mssql_config WHERE id = 1', (err, existing) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Lesen der bestehenden MSSQL-Konfiguration' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPassword =
|
||||||
|
trimmedPassword === null || trimmedPassword === ''
|
||||||
|
? (existing ? existing.password : '')
|
||||||
|
: trimmedPassword;
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
db.run(
|
||||||
|
'UPDATE mssql_config SET server = ?, database = ?, username = ?, password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
||||||
|
[trimmedServer, trimmedDatabase, trimmedUsername, newPassword],
|
||||||
|
(updateErr) => {
|
||||||
|
if (updateErr) {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern der MSSQL-Konfiguration' });
|
||||||
|
}
|
||||||
|
return res.json({ success: true });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
db.run(
|
||||||
|
'INSERT INTO mssql_config (id, server, database, username, password) VALUES (1, ?, ?, ?, ?)',
|
||||||
|
[trimmedServer, trimmedDatabase, trimmedUsername, newPassword || ''],
|
||||||
|
(insertErr) => {
|
||||||
|
if (insertErr) {
|
||||||
|
return res.status(500).json({ error: 'Fehler beim Speichern der MSSQL-Konfiguration' });
|
||||||
|
}
|
||||||
|
return res.json({ success: true });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// MSSQL-Verbindung testen
|
||||||
|
app.post('/admin/mssql-test-connection', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await testMssqlConnection();
|
||||||
|
return res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('MSSQL Testverbindung fehlgeschlagen:', err);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Verbindung zur MSSQL-Datenbank fehlgeschlagen: ' + err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = registerAdminRoutes;
|
module.exports = registerAdminRoutes;
|
||||||
|
|||||||
42
routes/project-search-routes.js
Normal file
42
routes/project-search-routes.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const { requireAuth } = require('../middleware/auth');
|
||||||
|
const { searchProjectsByDescription } = require('../services/mssql-infra-service');
|
||||||
|
|
||||||
|
function registerProjectSearchRoutes(app) {
|
||||||
|
// Seite für Projektsuche – für alle eingeloggten Nutzer
|
||||||
|
app.get('/projects/search', requireAuth, (req, res) => {
|
||||||
|
res.render('project-search', {
|
||||||
|
user: {
|
||||||
|
id: req.session.userId,
|
||||||
|
firstname: req.session.firstname,
|
||||||
|
lastname: req.session.lastname,
|
||||||
|
username: req.session.username,
|
||||||
|
roles: req.session.roles || [],
|
||||||
|
currentRole: req.session.currentRole || 'mitarbeiter'
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API-Endpunkt für Projektsuche über MSSQL
|
||||||
|
app.get('/api/projects/search', requireAuth, async (req, res) => {
|
||||||
|
const term = (req.query.term || '').trim();
|
||||||
|
|
||||||
|
if (!term) {
|
||||||
|
return res.status(400).json({ error: 'Suchbegriff fehlt' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await searchProjectsByDescription(term);
|
||||||
|
res.json({ results });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler bei MSSQL-Projektsuche:', err.message);
|
||||||
|
return res.status(500).json({
|
||||||
|
error:
|
||||||
|
'Projektsuche ist aktuell nicht verfügbar. Bitte prüfen Sie die MSSQL-Konfiguration im Adminbereich oder versuchen Sie es später erneut.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerProjectSearchRoutes;
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ const registerAdminRoutes = require('./routes/admin-routes');
|
|||||||
const registerAdminLDAPRoutes = require('./routes/admin-ldap-routes');
|
const registerAdminLDAPRoutes = require('./routes/admin-ldap-routes');
|
||||||
const registerVerwaltungRoutes = require('./routes/verwaltung-routes');
|
const registerVerwaltungRoutes = require('./routes/verwaltung-routes');
|
||||||
const registerTimesheetRoutes = require('./routes/timesheet-routes');
|
const registerTimesheetRoutes = require('./routes/timesheet-routes');
|
||||||
|
const registerProjectSearchRoutes = require('./routes/project-search-routes');
|
||||||
|
|
||||||
// Services importieren
|
// Services importieren
|
||||||
const { setupPingService } = require('./services/ping-service');
|
const { setupPingService } = require('./services/ping-service');
|
||||||
@@ -60,6 +61,7 @@ registerAdminRoutes(app);
|
|||||||
registerAdminLDAPRoutes(app);
|
registerAdminLDAPRoutes(app);
|
||||||
registerVerwaltungRoutes(app);
|
registerVerwaltungRoutes(app);
|
||||||
registerTimesheetRoutes(app);
|
registerTimesheetRoutes(app);
|
||||||
|
registerProjectSearchRoutes(app);
|
||||||
|
|
||||||
// Start-Route
|
// Start-Route
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
|
|||||||
91
services/mssql-infra-service.js
Normal file
91
services/mssql-infra-service.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const { db } = require('../database');
|
||||||
|
const sql = require('mssql');
|
||||||
|
|
||||||
|
let cachedConfig = null;
|
||||||
|
let lastConfigLoad = 0;
|
||||||
|
let pool = null;
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.get('SELECT server, database, username, password FROM mssql_config WHERE id = 1', (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(new Error('Fehler beim Lesen der MSSQL-Konfiguration: ' + err.message));
|
||||||
|
}
|
||||||
|
resolve(row || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMssqlPool() {
|
||||||
|
const now = Date.now();
|
||||||
|
if (!cachedConfig || now - lastConfigLoad > 60 * 1000) {
|
||||||
|
cachedConfig = await loadConfig();
|
||||||
|
lastConfigLoad = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cachedConfig || !cachedConfig.server || !cachedConfig.database || !cachedConfig.username || !cachedConfig.password) {
|
||||||
|
throw new Error('MSSQL-Konfiguration ist unvollständig. Bitte im Adminbereich konfigurieren.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool && pool.connected) {
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
server: cachedConfig.server,
|
||||||
|
database: cachedConfig.database,
|
||||||
|
user: cachedConfig.username,
|
||||||
|
password: cachedConfig.password,
|
||||||
|
options: {
|
||||||
|
encrypt: false,
|
||||||
|
trustServerCertificate: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pool = await sql.connect(config);
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchProjectsByDescription(searchTerm) {
|
||||||
|
const pool = await getMssqlPool();
|
||||||
|
const request = pool.request();
|
||||||
|
request.input('search', sql.NVarChar, `%${searchTerm}%`);
|
||||||
|
|
||||||
|
// Datenbankname aus Konfiguration verwenden
|
||||||
|
const dbName = cachedConfig && cachedConfig.database ? cachedConfig.database : null;
|
||||||
|
if (!dbName) {
|
||||||
|
throw new Error('MSSQL-Datenbankname ist nicht konfiguriert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT TOP 50
|
||||||
|
kk.Auftrag AS auftrag,
|
||||||
|
kk.Proj AS proj,
|
||||||
|
kk.Such AS such,
|
||||||
|
kk.Knd AS knd,
|
||||||
|
k.Bez AS bez
|
||||||
|
FROM [${dbName}].dbo.KKOPF kk
|
||||||
|
LEFT JOIN [${dbName}].dbo.KUNDE k ON kk.Knd = k.Knd
|
||||||
|
WHERE kk.Proj LIKE @search
|
||||||
|
OR kk.Auftrag LIKE @search
|
||||||
|
OR kk.Such LIKE @search
|
||||||
|
OR k.Bez LIKE @search
|
||||||
|
ORDER BY kk.ErfTerm DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await request.query(query);
|
||||||
|
return result.recordset || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testMssqlConnection() {
|
||||||
|
const pool = await getMssqlPool();
|
||||||
|
await pool.request().query('SELECT TOP 1 1 AS ok');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getMssqlPool,
|
||||||
|
searchProjectsByDescription,
|
||||||
|
testMssqlConnection
|
||||||
|
};
|
||||||
|
|
||||||
@@ -392,6 +392,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mssql-config-section" style="margin-top: 40px;">
|
||||||
|
<div class="collapsible-header" onclick="toggleMssqlSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h2 style="margin: 0;">MSSQL-Projektsuche Konfiguration</h2>
|
||||||
|
<span id="mssqlToggleIcon" style="font-size: 18px; transition: transform 0.3s;">▼</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mssqlContent" style="display: none; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff;">
|
||||||
|
<div class="mssql-config-form">
|
||||||
|
<h3>MSSQL-Verbindungsdaten</h3>
|
||||||
|
<p style="margin-bottom: 20px; color: #666;">
|
||||||
|
Konfigurieren Sie hier die Verbindung zur MSSQL-Datenbank, aus der die Projektnummern ermittelt werden
|
||||||
|
(z.B. Tabelle <code>infra.dbo.KKOPF</code> und <code>KUNDE</code>).
|
||||||
|
</p>
|
||||||
|
<form id="mssqlConfigForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mssqlServer">Server</label>
|
||||||
|
<input type="text" id="mssqlServer" name="server" placeholder="z.B. SERVER\\INSTANZ oder server.domain.local">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mssqlDatabase">Datenbankname</label>
|
||||||
|
<input type="text" id="mssqlDatabase" name="database" placeholder="z.B. infra">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mssqlUsername">Benutzername</label>
|
||||||
|
<input type="text" id="mssqlUsername" name="username" placeholder="Datenbank-Benutzer">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mssqlPassword">Passwort</label>
|
||||||
|
<input type="password" id="mssqlPassword" name="password" placeholder="Leer lassen, um aktuelles Passwort zu behalten">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (mssqlConfig && mssqlConfig.updated_at) { %>
|
||||||
|
<p style="margin-top: 10px; color: #666;">
|
||||||
|
<strong>Zuletzt geändert:</strong>
|
||||||
|
<%= new Date(mssqlConfig.updated_at).toLocaleString('de-DE') %>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">MSSQL-Konfiguration speichern</button>
|
||||||
|
<button type="button" id="mssqlTestConnectionBtn" class="btn btn-secondary" style="margin-left: 10px;">Verbindung testen</button>
|
||||||
|
<span id="mssqlTestStatus" style="margin-left: 10px;"></span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -454,6 +503,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMssqlSection() {
|
||||||
|
const content = document.getElementById('mssqlContent');
|
||||||
|
const icon = document.getElementById('mssqlToggleIcon');
|
||||||
|
|
||||||
|
if (content.style.display === 'none') {
|
||||||
|
content.style.display = 'block';
|
||||||
|
icon.style.transform = 'rotate(180deg)';
|
||||||
|
} else {
|
||||||
|
content.style.display = 'none';
|
||||||
|
icon.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rollenwechsel-Handler
|
// Rollenwechsel-Handler
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const roleSwitcher = document.getElementById('roleSwitcher');
|
const roleSwitcher = document.getElementById('roleSwitcher');
|
||||||
|
|||||||
169
views/project-search.ejs
Normal file
169
views/project-search.ejs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Projektsuche</title>
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<%- include('header') %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<img src="/images/header.png" alt="Logo" class="navbar-logo">
|
||||||
|
<h1>Stundenerfassung - Projektsuche</h1>
|
||||||
|
</div>
|
||||||
|
<div class="nav-right">
|
||||||
|
<span><%= user.firstname %> <%= user.lastname %></span>
|
||||||
|
<a href="/logout" class="btn btn-logout">Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" style="margin-top: 30px;">
|
||||||
|
<h2>Projektsuche</h2>
|
||||||
|
<p>
|
||||||
|
Suchen Sie nach Projekten über die Beschreibung (<code>Proj</code>).
|
||||||
|
Die Projektnummer stammt aus <code>Auftrag</code>, die Kundenbezeichnung aus <code>KUNDE.Bez</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if (error) { %>
|
||||||
|
<div class="alert alert-danger"><%= error %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<form id="searchForm" class="form-inline" style="margin-top: 15px; margin-bottom: 15px;">
|
||||||
|
<label for="searchTerm" style="margin-right: 10px;">Suchbegriff in Projektbeschreibung:</label>
|
||||||
|
<input type="text" id="searchTerm" name="term" required style="min-width: 250px; margin-right: 10px;">
|
||||||
|
<button type="submit" class="btn btn-primary">Suchen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="message" style="margin-top: 10px; color: #c00;"></div>
|
||||||
|
|
||||||
|
<table id="resultsTable" style="width:100%; margin-top:1rem; border-collapse: collapse; display:none;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="border-bottom:1px solid #ccc; text-align:left;">Projektnummer (Auftrag)</th>
|
||||||
|
<th style="border-bottom:1px solid #ccc; text-align:left;">Beschreibung (Proj)</th>
|
||||||
|
<th style="border-bottom:1px solid #ccc; text-align:left;">Seriennummer</th>
|
||||||
|
<th style="border-bottom:1px solid #ccc; text-align:left;">Kundennummer (Knd)</th>
|
||||||
|
<th style="border-bottom:1px solid #ccc; text-align:left;">Kundenbezeichnung (Bez)</th>
|
||||||
|
<th style="border-bottom:1px solid #ccc; text-align:left;">Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById('searchForm');
|
||||||
|
const messageEl = document.getElementById('message');
|
||||||
|
const table = document.getElementById('resultsTable');
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const term = document.getElementById('searchTerm').value.trim();
|
||||||
|
messageEl.style.color = '#c00';
|
||||||
|
messageEl.textContent = '';
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
table.style.display = 'none';
|
||||||
|
|
||||||
|
if (!term) {
|
||||||
|
messageEl.textContent = 'Bitte einen Suchbegriff eingeben.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ term });
|
||||||
|
const response = await fetch('/api/projects/search?' + params.toString(), {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
messageEl.textContent = result.error || 'Fehler bei der Projektsuche.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = result.results || [];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
messageEl.textContent = 'Keine Projekte gefunden.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const auftrag = row.auftrag || '';
|
||||||
|
const proj = row.proj || '';
|
||||||
|
const such = row.such || '';
|
||||||
|
const knd = row.knd || '';
|
||||||
|
const bez = row.bez || '';
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${auftrag}</td>
|
||||||
|
<td>${proj}</td>
|
||||||
|
<td>${such}</td>
|
||||||
|
<td>${knd}</td>
|
||||||
|
<td>${bez}</td>
|
||||||
|
<td><button type="button" class="btn btn-secondary btn-sm copy-btn" data-auftrag="${auftrag}">Kopieren</button></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.style.display = '';
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler bei der Projektsuche:', err);
|
||||||
|
messageEl.textContent = 'Fehler bei der Projektsuche. Bitte später erneut versuchen.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||||
|
return navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
var ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
ta.setAttribute('readonly', '');
|
||||||
|
ta.style.position = 'absolute';
|
||||||
|
ta.style.left = '-9999px';
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e);
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.addEventListener('click', (ev) => {
|
||||||
|
const btn = ev.target.closest('button.copy-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
const value = btn.getAttribute('data-auftrag');
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
copyToClipboard(value).then(() => {
|
||||||
|
messageEl.style.color = 'green';
|
||||||
|
messageEl.textContent = 'Projektnummer wurde in die Zwischenablage kopiert.';
|
||||||
|
setTimeout(() => {
|
||||||
|
messageEl.textContent = '';
|
||||||
|
messageEl.style.color = '#c00';
|
||||||
|
}, 2000);
|
||||||
|
}).catch(() => {
|
||||||
|
messageEl.textContent = 'Kopieren nicht möglich.';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%- include('footer') %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Reference in New Issue
Block a user