Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IT Nexus Ticket erstellen</title>
<script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', sans-serif; font-size: 13px; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
.container { padding: 16px; }
.logo { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; font-size: 15px; font-weight: 600; color: #7c9ef8; }
label { display: block; font-size: 11px; color: #9aa3b2; margin-bottom: 4px; margin-top: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
input, textarea, select { width: 100%; padding: 8px 10px; background: #252540; border: 1px solid #3a3a5c; border-radius: 6px; color: #e0e0e0; font-size: 13px; font-family: inherit; outline: none; transition: border-color 0.2s; }
input:focus, textarea:focus, select:focus { border-color: #7c9ef8; }
textarea { resize: vertical; min-height: 80px; }
select option { background: #252540; }
.btn { display: block; width: 100%; padding: 10px; margin-top: 14px; background: #7c9ef8; color: #fff; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
.btn:hover { background: #5b7ef5; }
.btn:disabled { background: #3a3a5c; cursor: not-allowed; }
.btn-secondary { background: transparent; border: 1px solid #3a3a5c; color: #9aa3b2; margin-top: 8px; }
.btn-secondary:hover { border-color: #7c9ef8; color: #7c9ef8; }
.btn-ms { background: #2f6baf; display: flex; align-items: center; justify-content: center; gap: 8px; }
.btn-ms:hover { background: #1e5a9e; }
.alert { padding: 10px 12px; border-radius: 6px; margin-top: 12px; font-size: 12px; }
.alert-success { background: #1a3a2a; border: 1px solid #2a6a4a; color: #5edb9a; }
.alert-error { background: #3a1a1a; border: 1px solid #6a2a2a; color: #f08080; }
.alert-info { background: #1a2a3a; border: 1px solid #2a4a6a; color: #7cb8f8; }
#login-section, #form-section, #success-section { display: none; }
.loading { text-align: center; padding: 30px; color: #9aa3b2; }
.ticket-num { font-size: 20px; font-weight: 700; color: #7c9ef8; text-align: center; margin: 10px 0 4px; }
.ticket-sub { text-align: center; font-size: 12px; color: #9aa3b2; }
</style>
</head>
<body>
<div class="container">
<div class="logo">🎫 IT Nexus</div>
<!-- Login -->
<div id="login-section">
<div class="alert alert-info" style="margin-bottom:12px;">Mit Microsoft-Konto anmelden</div>
<button class="btn btn-ms" id="btn-ms-login">
<svg width="16" height="16" viewBox="0 0 21 21" fill="none"><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>
Mit Microsoft anmelden
</button>
<div id="login-error"></div>
</div>
<!-- Ticket-Formular -->
<div id="form-section">
<div id="user-info" style="font-size:11px;color:#9aa3b2;margin-bottom:8px;"></div>
<label>Titel</label>
<input type="text" id="f-title" maxlength="200" />
<label>Beschreibung</label>
<textarea id="f-desc" rows="5"></textarea>
<label>Kategorie</label>
<select id="f-category">
<option value="Allgemein">Allgemein</option>
<option value="Hardware">Hardware</option>
<option value="Software">Software</option>
<option value="Netzwerk">Netzwerk</option>
<option value="Zugang & Berechtigungen">Zugang &amp; Berechtigungen</option>
<option value="E-Mail">E-Mail</option>
<option value="Drucker">Drucker</option>
<option value="Sonstiges">Sonstiges</option>
</select>
<label>Priorität</label>
<select id="f-priority">
<option value="niedrig">Niedrig</option>
<option value="mittel" selected>Mittel</option>
<option value="hoch">Hoch</option>
<option value="kritisch">Kritisch</option>
</select>
<button class="btn" id="btn-submit">Ticket erstellen</button>
<button class="btn btn-secondary" id="btn-logout">Abmelden</button>
<div id="form-error"></div>
</div>
<!-- Erfolg -->
<div id="success-section">
<div class="alert alert-success" style="text-align:center;padding:20px;">
<div style="font-size:28px;margin-bottom:8px;"></div>
<div style="font-weight:600;margin-bottom:4px;">Ticket erstellt!</div>
<div class="ticket-num" id="s-number"></div>
<div class="ticket-sub" id="s-title"></div>
</div>
<button class="btn btn-secondary" id="btn-new">Weiteres Ticket erstellen</button>
</div>
<div id="loading-section" class="loading">⏳ Wird geladen…</div>
</div>
<script>
let token = localStorage.getItem('it_nexus_addin_token') || null;
let currentUser = JSON.parse(localStorage.getItem('it_nexus_addin_user') || 'null');
function show(id) {
['login-section','form-section','success-section','loading-section'].forEach(s => {
document.getElementById(s).style.display = s === id ? 'block' : 'none';
});
}
function setError(elId, msg) {
document.getElementById(elId).innerHTML = msg ? `<div class="alert alert-error">${msg}</div>` : '';
}
// ── Microsoft SSO Login (Polling-basiert, kein messageParent) ─────────────
document.getElementById('btn-ms-login').addEventListener('click', () => {
show('loading-section');
const sessionId = Date.now().toString(36) + Math.random().toString(36).slice(2);
const dialogUrl = 'https://it-nexus.cereda-systems.de/api/auth/microsoft/addin?sessionId=' + sessionId;
Office.context.ui.displayDialogAsync(
dialogUrl,
{ height: 60, width: 30, promptBeforeOpen: false },
(result) => {
if (result.status === Office.AsyncResultStatus.Failed) {
show('login-section');
setError('login-error', 'Popup konnte nicht geöffnet werden.');
return;
}
const dialog = result.value;
let pollInterval = null;
const handleResult = (data) => {
if (pollInterval) clearInterval(pollInterval);
try { dialog.close(); } catch(e) {}
if (data.error) {
show('login-section');
const msgs = {
user_not_found: 'Kein IT Nexus Konto gefunden.',
account_inactive: 'Konto ist deaktiviert.',
};
setError('login-error', msgs[data.error] || 'Fehler: ' + data.error);
return;
}
token = data.token;
currentUser = data.user;
localStorage.setItem('it_nexus_addin_token', token);
localStorage.setItem('it_nexus_addin_user', JSON.stringify(currentUser));
loadEmailAndShowForm();
};
// Poll server every 2 seconds for login result
pollInterval = setInterval(async () => {
try {
const r = await fetch('https://it-nexus.cereda-systems.de/api/auth/microsoft/addin-result/' + sessionId);
const data = await r.json();
if (!data.pending) handleResult(data);
} catch(e) {}
}, 2000);
// Also handle if user manually closes dialog
dialog.addEventHandler(Office.EventType.DialogEventReceived, () => {
if (pollInterval) clearInterval(pollInterval);
show('login-section');
});
// Keep messageParent as fallback
dialog.addEventHandler(Office.EventType.DialogMessageReceived, (msg) => {
try { handleResult(JSON.parse(msg.message)); } catch(e) {}
});
}
);
});
// ── Logout ─────────────────────────────────────────────────────────────────
document.getElementById('btn-logout').addEventListener('click', () => {
token = null; currentUser = null;
localStorage.removeItem('it_nexus_addin_token');
localStorage.removeItem('it_nexus_addin_user');
show('login-section');
});
// ── Ticket erstellen ───────────────────────────────────────────────────────
document.getElementById('btn-submit').addEventListener('click', async () => {
const title = document.getElementById('f-title').value.trim();
if (!title) return setError('form-error', 'Titel ist erforderlich.');
document.getElementById('btn-submit').disabled = true;
setError('form-error', '');
try {
const res = await fetch('/api/tickets', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({
title,
description: document.getElementById('f-desc').value.trim(),
category: document.getElementById('f-category').value,
priority: document.getElementById('f-priority').value,
source: 'email',
}),
});
const data = await res.json();
if (!res.ok) {
if (res.status === 401) { token = null; localStorage.removeItem('it_nexus_addin_token'); show('login-section'); return; }
throw new Error(data.message || 'Fehler beim Erstellen');
}
document.getElementById('s-number').textContent = data.data.ticket_number;
document.getElementById('s-title').textContent = title;
show('success-section');
} catch (e) {
setError('form-error', e.message);
} finally {
document.getElementById('btn-submit').disabled = false;
}
});
document.getElementById('btn-new').addEventListener('click', () => loadEmailAndShowForm());
// ── E-Mail laden ───────────────────────────────────────────────────────────
function loadEmailAndShowForm() {
show('loading-section');
if (currentUser) {
document.getElementById('user-info').textContent =
`Angemeldet als: ${currentUser.first_name || ''} ${currentUser.last_name || currentUser.username}`.trim();
}
if (typeof Office !== 'undefined' && Office.context?.mailbox) {
const item = Office.context.mailbox.item;
document.getElementById('f-title').value = item.subject || '';
item.body.getAsync(Office.CoercionType.Text, {}, (r) => {
document.getElementById('f-desc').value =
r.status === Office.AsyncResultStatus.Succeeded ? r.value.substring(0, 2000).trim() : '';
show('form-section');
});
} else {
document.getElementById('f-title').value = '';
document.getElementById('f-desc').value = '';
show('form-section');
}
}
// ── Init ───────────────────────────────────────────────────────────────────
Office.onReady(async () => {
if (token) {
try {
const res = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) { loadEmailAndShowForm(); return; }
} catch {}
token = null;
localStorage.removeItem('it_nexus_addin_token');
}
show('login-section');
});
</script>
</body>
</html>