Ein Computer im Spiel. Man tritt an ein Terminal, drueckt E, und bekommt eine
Oberflaeche mit Mail, Browser, Adressbuch, Kalender und weiteren Anwendungen.
Andere Resources haengen sich als App ein.
Auf ESX umgestellt:
- Fuenf harte Abhaengigkeiten zeigten auf Resources, die es hier nicht gibt.
Eine fehlende Abhaengigkeit verhindert den Start vollstaendig - die
Resource waere nie hochgekommen.
- Die Bruecke zum Framework neu geschrieben; Charakterdaten, Aktenverwaltung
und Immobilien ausgebaut, weil die zugehoerigen Systeme fehlen.
- pc_live_devices fehlte im Schema, wird vom Code aber gelesen. Ohne die
Tabelle ist kein fester PC ansprechbar. Ein PC braucht ausserdem zwei
Eintraege mit derselben Kennung: Standort in der Datenbank, Kamerafahrt in
der config.lua.
Mail ueberarbeitet:
- Postfaecher haengen an einer Anmeldung statt an der Person. Mehrere Leute
koennen dasselbe Firmenpostfach gleichzeitig offen haben.
- Ordner gehoeren zum Postfach, nicht zur Person. Eine Mail an ein geteiltes
Postfach wird je Empfaenger einmal gespeichert; damit das Einsortieren
trotzdem fuer alle gilt, tragen alle Kopien einer Zustellung dieselbe
Kennung. Gelesen und geloescht bleibt persoenlich - das ist keine
Eigenschaft der Nachricht, sondern der Person.
- Signaturen gehoeren ebenfalls zum Postfach.
- Kalender koennen privat, geteilt oder oeffentlich sein. Ein oeffentlicher
Termin haengt bewusst auch an einem Postfach, sonst koennte ihn spaeter
niemand mehr aendern oder absagen.
Neue Apps: Webhosting, Bleeter und dessen Verwaltung.
Behoben:
- prompt(), confirm() und alert() froren das NUI ein. FiveMs CEF hat keinen
Handler fuer die eingebauten Browserdialoge - der Aufruf oeffnet nichts und
kehrt nie zurueck. Ersetzt durch eigene Dialoge.
- Die Fenster tragen data-wid, keine id. Wer sie mit getElementById sucht,
schreibt ins Leere und das Fenster bleibt leer.
Enthaelt README.md mit Einrichtung und PLUGINS.md: eine Anleitung, wie man eine
eigene App baut und einhaengt, mit vollstaendigem Beispiel von der Tabelle bis
zum Schreibtischsymbol.
1153 lines
48 KiB
JavaScript
1153 lines
48 KiB
JavaScript
/**
|
||
* pc-live | Mail App
|
||
* Multi-mailbox: persönliches Postfach + geteilte Postfächer (ic-mail)
|
||
*/
|
||
const MailApp = (() => {
|
||
const WIN_ID = 'app-mail';
|
||
|
||
// ── Mail state ──────────────────────────────────────────
|
||
let _inbox = [];
|
||
let _sent = [];
|
||
// Ordner gehören zum Postfach, nicht zur Person: ein geteiltes Postfach
|
||
// wird von mehreren bedient und hat für alle dieselbe Struktur.
|
||
// Schlüssel: Adresse, '' = persönliches Postfach.
|
||
let _foldersByBox = {};
|
||
let _folderMails = {};
|
||
let _calendar = [];
|
||
let _active = null;
|
||
let _view = 'inbox';
|
||
let _editEvent = null;
|
||
let _contacts = [];
|
||
|
||
// ── Mailbox state (ic-mail) ─────────────────────────────
|
||
let _mailboxes = []; // [{address, display_name, type, realm_type}]
|
||
let _personalAddr = ''; // primäre persönliche Adresse
|
||
let _activeAddr = null; // null = persönlich, sonst Adress-String
|
||
let _personalSig = ''; // Signatur des persönlichen Postfachs
|
||
let _sigAddr = ''; // Postfach, dessen Signatur gerade bearbeitet wird
|
||
let _openBoxes = []; // per Passwort freigeschaltete Postfächer (ic-web)
|
||
|
||
/* ── Helpers ──────────────────────────────────────────── */
|
||
function fmtDate(d) {
|
||
if (!d) return '';
|
||
const dt = new Date(d);
|
||
return isNaN(dt) ? d : dt.toLocaleString('de-DE', {
|
||
day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit'
|
||
});
|
||
}
|
||
function fmtDateInput(d) {
|
||
if (!d) return '';
|
||
const dt = new Date(d);
|
||
if (isNaN(dt)) return '';
|
||
return dt.toISOString().slice(0, 16);
|
||
}
|
||
function esc(s) {
|
||
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
/* Adresse des gerade gewählten Postfachs, '' = persönlich. */
|
||
function boxKey() { return _activeAddr || ''; }
|
||
|
||
/* Ordner des gewählten Postfachs. */
|
||
function currentFolders() { return _foldersByBox[boxKey()] || []; }
|
||
|
||
/* Signatur eines Postfachs. '' wenn keine gepflegt ist. */
|
||
function signatureFor(address) {
|
||
if (!address || address === _personalAddr) return _personalSig || '';
|
||
const mb = _mailboxes.find(m => m.address === address);
|
||
return (mb && mb.signature) || '';
|
||
}
|
||
|
||
/* Postfächer, die über "Postfach hinzufügen" freigeschaltet wurden und sich
|
||
deshalb auch wieder entfernen lassen. Das eigene und dienstlich
|
||
zugewiesene gehören einem nicht. */
|
||
function isRemovable(address) {
|
||
return _openBoxes.includes(address);
|
||
}
|
||
|
||
/* Trennzeile vor der Signatur – wie in jedem Mailprogramm. */
|
||
const SIG_SEP = '\n\n-- \n';
|
||
|
||
/* Alten Signaturblock abschneiden und den neuen anhängen. So bleibt der
|
||
geschriebene Text erhalten, wenn man den Absender wechselt. */
|
||
function applySignature(text, address) {
|
||
const idx = text.lastIndexOf(SIG_SEP);
|
||
const body = idx >= 0 ? text.slice(0, idx) : text;
|
||
const sig = signatureFor(address);
|
||
return sig ? body + SIG_SEP + sig : body;
|
||
}
|
||
function currentFolderId() {
|
||
if (_view.startsWith('folder:')) return parseInt(_view.split(':')[1]);
|
||
return null;
|
||
}
|
||
|
||
// Mails des aktiven Postfachs aus dem gesamten Inbox-Array filtern
|
||
function _mailboxFilter(mails) {
|
||
const sharedSet = new Set(_mailboxes.filter(m => m.type !== 'personal').map(m => m.address));
|
||
if (!_activeAddr) {
|
||
// Persönlich: alles was NICHT zu einem geteilten Postfach gehört
|
||
return mails.filter(m => !m.to_address || !sharedSet.has(m.to_address));
|
||
}
|
||
return mails.filter(m => m.to_address === _activeAddr);
|
||
}
|
||
|
||
function currentMails() {
|
||
if (_view === 'inbox') return _mailboxFilter(_inbox);
|
||
if (_view === 'sent') {
|
||
if (!_activeAddr) {
|
||
const sharedSet = new Set(_mailboxes.filter(m => m.type !== 'personal').map(m => m.address));
|
||
return _sent.filter(m => !sharedSet.has(m.from_identifier));
|
||
}
|
||
return _sent.filter(m => m.from_identifier === _activeAddr);
|
||
}
|
||
const fid = currentFolderId();
|
||
if (fid != null) return _mailboxFilter(_folderMails[fid] || []);
|
||
return [];
|
||
}
|
||
|
||
/* ── Mailbox icon ─────────────────────────────────────── */
|
||
function _mbIcon(mb) {
|
||
if (mb.type === 'personal') return '✉';
|
||
if (mb.realm_type === 'faction') return '🏛';
|
||
if (mb.realm_type === 'company') return '🏢';
|
||
return '📮';
|
||
}
|
||
|
||
/* ── Sidebar ──────────────────────────────────────────── */
|
||
function renderSidebar() {
|
||
const fid = currentFolderId();
|
||
|
||
// Unread pro Postfach berechnen
|
||
const sharedSet = new Set(_mailboxes.filter(m => m.type !== 'personal').map(m => m.address));
|
||
const personalUnread = _inbox.filter(m => !m.is_read && (!m.to_address || !sharedSet.has(m.to_address))).length;
|
||
|
||
// Persönliches Postfach
|
||
const personalLabel = _personalAddr || 'Mein Postfach';
|
||
const personalActive = !_activeAddr;
|
||
let mailboxItems = `
|
||
<div class="sidebar-item ${personalActive ? 'active' : ''}" onclick="MailApp._setMailbox(null)">
|
||
<span>✉ ${esc(personalLabel)}</span>
|
||
${personalUnread > 0 ? `<span class="badge" style="margin-left:auto">${personalUnread}</span>` : ''}
|
||
</div>`;
|
||
|
||
// Geteilte Postfächer
|
||
for (const mb of _mailboxes) {
|
||
if (mb.type === 'personal') continue;
|
||
const mbUnread = _inbox.filter(m => m.to_address === mb.address && !m.is_read).length;
|
||
const isActive = _activeAddr === mb.address;
|
||
const label = mb.display_name || mb.address;
|
||
const removeBtn = isRemovable(mb.address) ? `
|
||
<button class="sidebar-folder-del" title="Postfach entfernen"
|
||
onclick="event.stopPropagation();MailApp._removeMailbox('${esc(mb.address)}')">✕</button>` : '';
|
||
mailboxItems += `
|
||
<div class="sidebar-item ${isActive ? 'active' : ''}" onclick="MailApp._setMailbox('${esc(mb.address)}')">
|
||
<span>${_mbIcon(mb)} ${esc(label)}</span>
|
||
${mbUnread > 0 ? `<span class="badge" style="margin-left:auto">${mbUnread}</span>` : ''}
|
||
${removeBtn}
|
||
</div>`;
|
||
}
|
||
|
||
mailboxItems += `
|
||
<div class="sidebar-item" style="color:var(--text-dim)" onclick="MailApp._addMailbox()">
|
||
<span>+ Postfach hinzufügen</span>
|
||
</div>`;
|
||
|
||
// Navigation für aktives Postfach
|
||
const viewMails = currentMails();
|
||
const viewUnread = _view === 'inbox' ? viewMails.filter(m => !m.is_read).length : 0;
|
||
|
||
// Ordner
|
||
let folderItems = currentFolders().map(f => `
|
||
<div class="sidebar-item sidebar-folder-item ${_view === 'folder:' + f.id ? 'active' : ''}"
|
||
onclick="MailApp._setView('folder:${f.id}')">
|
||
<span>📁 ${esc(f.name)}</span>
|
||
<button class="sidebar-folder-del" title="Ordner löschen"
|
||
onclick="event.stopPropagation();MailApp._deleteFolder(${f.id})">✕</button>
|
||
</div>`).join('');
|
||
|
||
return `
|
||
<div class="app-sidebar" style="min-width:175px;max-width:215px">
|
||
<div class="sidebar-section" style="display:flex;align-items:center;justify-content:space-between">
|
||
<span>Postfächer</span>
|
||
<button class="btn-ghost" style="padding:1px 6px;font-size:.65rem" title="Postfächer aktualisieren"
|
||
onclick="fetchNui('getSharedMailboxes',{})">↻</button>
|
||
</div>
|
||
${mailboxItems}
|
||
|
||
<div class="divider" style="margin:8px 12px"></div>
|
||
|
||
<div class="sidebar-section">Navigation</div>
|
||
<div class="sidebar-item ${_view==='inbox' ? 'active':''}" onclick="MailApp._setView('inbox')">
|
||
📥 Posteingang ${viewUnread > 0 ? `<span class="badge" style="margin-left:auto">${viewUnread}</span>` : ''}
|
||
</div>
|
||
<div class="sidebar-item ${_view==='sent' ? 'active':''}" onclick="MailApp._setView('sent')">
|
||
📤 Gesendet
|
||
</div>
|
||
<div class="sidebar-item ${_view==='compose' ? 'active':''}" onclick="MailApp._setView('compose')">
|
||
✏ Schreiben
|
||
</div>
|
||
<div class="sidebar-item ${_view==='signature' ? 'active':''}" onclick="MailApp._setView('signature')">
|
||
✒ Signatur
|
||
</div>
|
||
<div class="sidebar-section" style="margin-top:8px;display:flex;align-items:center;justify-content:space-between">
|
||
<span>Ordner</span>
|
||
<button class="btn-ghost" style="padding:1px 6px;font-size:.7rem" onclick="MailApp._promptNewFolder()">+</button>
|
||
</div>
|
||
${folderItems}
|
||
|
||
<div class="divider" style="margin:8px 12px"></div>
|
||
<div class="sidebar-section">Kalender</div>
|
||
<div class="sidebar-item ${_view==='calendar'? 'active':''}" onclick="MailApp._setView('calendar')">
|
||
📅 Kalender
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Mail list ────────────────────────────────────────── */
|
||
function renderMailList(mails, isSent) {
|
||
if (!mails.length)
|
||
return `<div class="empty-state"><div class="empty-icon">📭</div><p>Keine Nachrichten</p></div>`;
|
||
|
||
const fid = currentFolderId();
|
||
const folderOptions = currentFolders().map(f =>
|
||
`<option value="${f.id}">${esc(f.name)}</option>`).join('');
|
||
const moveSelect = (fid == null && !isSent) ? `
|
||
<select class="mail-move-select" title="In Ordner verschieben"
|
||
onchange="MailApp._moveMail(${'{id}'}, this.value); this.selectedIndex=0">
|
||
<option value="">📁 Verschieben…</option>
|
||
${folderOptions}
|
||
</select>` : '';
|
||
const toInboxBtn = (fid != null) ? `
|
||
<button class="btn-ghost btn-sm" onclick="MailApp._moveMail(${'{id}'}, null)" title="Zurück in Posteingang">↩ Posteingang</button>` : '';
|
||
|
||
return `<div class="mail-list">` + mails.map(m => {
|
||
const addr = isSent
|
||
? esc(m.to_address || m.to_identifier)
|
||
: esc(m.from_identifier);
|
||
const label = isSent ? `An: ${addr}` : addr;
|
||
const moveBtn = moveSelect.replaceAll('{id}', m.id);
|
||
const inboxBtn = toInboxBtn.replaceAll('{id}', m.id);
|
||
return `
|
||
<div class="mail-item ${m.is_read || isSent ? '' : 'unread'} ${_active===m.id ? 'active' : ''}"
|
||
onclick="MailApp._openMail(${m.id})">
|
||
<div class="mail-from">${label}</div>
|
||
<div class="mail-subject">${esc(m.subject)}</div>
|
||
<div style="display:flex;align-items:center;gap:4px;margin-top:2px">
|
||
<div class="mail-date" style="flex:1">${fmtDate(m.sent_at)}</div>
|
||
${moveBtn}${inboxBtn}
|
||
</div>
|
||
</div>`;
|
||
}).join('') + `</div>`;
|
||
}
|
||
|
||
/* ── Mail detail ──────────────────────────────────────── */
|
||
function renderMailDetail(mail, isSent) {
|
||
if (!mail)
|
||
return `<div class="empty-state"><div class="empty-icon">📧</div><p>Nachricht auswählen</p></div>`;
|
||
|
||
const fid = currentFolderId();
|
||
const folderOptions = currentFolders().map(f =>
|
||
`<option value="${f.id}">${esc(f.name)}</option>`).join('');
|
||
const moveRow = (!isSent) ? `
|
||
<select class="btn-ghost btn-sm" style="cursor:pointer"
|
||
onchange="MailApp._moveMail(${mail.id}, this.value||null); this.selectedIndex=0">
|
||
<option value="">📁 Verschieben…</option>
|
||
${folderOptions}
|
||
${fid!=null ? `<option value="">↩ Posteingang</option>` : ''}
|
||
</select>` : '';
|
||
|
||
const replyBtn = !isSent ? `<button class="btn-ghost btn-sm" onclick="MailApp._reply()">↩ Antworten</button>` : '';
|
||
const contactMatch = _contacts.find(c => c.email === (isSent ? mail.to_identifier : mail.from_identifier));
|
||
const contactBtn = !contactMatch ? `
|
||
<button class="btn-ghost btn-sm" onclick="MailApp._saveAsContact('${esc(isSent ? mail.to_identifier : mail.from_identifier)}')">
|
||
👤 Als Kontakt speichern
|
||
</button>` : '';
|
||
|
||
// Zeige Postfach-Badge wenn Shared-Mail
|
||
const mbBadge = mail.to_address && mail.to_address !== _personalAddr ? `
|
||
<span style="font-size:.7rem;background:#1a2a1a;border:1px solid #2a4a2a;color:#4caf50;
|
||
padding:2px 8px;border-radius:4px;margin-left:4px">
|
||
📮 ${esc(mail.to_address)}
|
||
</span>` : '';
|
||
|
||
return `
|
||
<div class="mail-detail fade-in">
|
||
<div class="mail-detail-header">
|
||
<div class="mail-detail-subject">${esc(mail.subject)}${mbBadge}</div>
|
||
<div class="mail-detail-meta">
|
||
<span>${isSent ? 'An' : 'Von'}: ${esc(isSent ? (mail.to_address || mail.to_identifier) : mail.from_identifier)}</span>
|
||
<span>Datum: ${fmtDate(mail.sent_at)}</span>
|
||
</div>
|
||
<div style="margin-top:8px;display:flex;gap:6px;flex-wrap:wrap">
|
||
${replyBtn}
|
||
${moveRow}
|
||
${contactBtn}
|
||
${!isSent ? `<button class="btn-danger btn-sm" onclick="MailApp._deleteMail(${mail.id})">🗑 Löschen</button>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="mail-detail-body">${esc(mail.body)}</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Signatur ─────────────────────────────────────────── */
|
||
function renderSignature() {
|
||
// Alle Postfächer, für die eine Signatur gepflegt werden kann.
|
||
const boxes = [];
|
||
if (_personalAddr) boxes.push({ address: _personalAddr, label: _personalAddr });
|
||
for (const mb of _mailboxes) {
|
||
if (mb.address === _personalAddr) continue;
|
||
boxes.push({ address: mb.address, label: mb.display_name
|
||
? `${mb.display_name} (${mb.address})` : mb.address });
|
||
}
|
||
|
||
if (!boxes.length) {
|
||
return `<div class="empty-state"><div class="empty-icon">✒</div>
|
||
<p>Für dieses Postfach gibt es keine Adresse.</p></div>`;
|
||
}
|
||
|
||
// Vorauswahl: das gewählte Postfach, sonst das zuletzt bearbeitete.
|
||
if (!boxes.some(b => b.address === _sigAddr)) {
|
||
_sigAddr = _activeAddr || _personalAddr || boxes[0].address;
|
||
}
|
||
const addr = _sigAddr;
|
||
|
||
const opts = boxes.map(b =>
|
||
`<option value="${esc(b.address)}" ${b.address === addr ? 'selected' : ''}>${esc(b.label)}</option>`
|
||
).join('');
|
||
|
||
return `
|
||
<div class="app-content fade-in" style="max-width:640px;padding:16px">
|
||
<h3 style="color:#fff;font-size:.9rem;margin-bottom:4px">✒ Signatur</h3>
|
||
<div class="form-group">
|
||
<label>Postfach</label>
|
||
<select id="mail-sig-box" onchange="MailApp._sigBoxChanged()">${opts}</select>
|
||
</div>
|
||
<div style="font-size:.72rem;color:var(--text-muted);margin-bottom:12px">
|
||
Die Signatur gehört zum Postfach: Wer es bedient, schreibt mit derselben
|
||
Fußzeile.
|
||
</div>
|
||
<div class="form-group">
|
||
<textarea id="mail-signature" rows="7" maxlength="1000"
|
||
placeholder="z. B. Mit freundlichen Grüßen Redaktion Weazel News presse@weazel-news.ls"
|
||
>${esc(signatureFor(addr))}</textarea>
|
||
</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn-primary" onclick="MailApp._saveSignature()">Speichern</button>
|
||
<button class="btn-ghost" onclick="MailApp._setView('inbox')">Zurück</button>
|
||
</div>
|
||
<div id="mail-sig-status" style="font-size:.72rem;margin-top:6px;color:var(--text-muted)"></div>
|
||
<div style="font-size:.68rem;color:var(--text-dim);margin-top:14px;line-height:1.5">
|
||
Beim Schreiben wird die Signatur unter den Text gesetzt, getrennt durch eine
|
||
Zeile mit <code>--</code>. Wechselst du den Absender, tauscht sie sich mit.
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function _sigBoxChanged() {
|
||
const sel = document.getElementById('mail-sig-box');
|
||
if (!sel) return;
|
||
_sigAddr = sel.value;
|
||
_refresh();
|
||
}
|
||
|
||
function _saveSignature() {
|
||
const addr = _sigAddr || _activeAddr || _personalAddr;
|
||
const el = document.getElementById('mail-signature');
|
||
const st = document.getElementById('mail-sig-status');
|
||
if (!addr || !el) return;
|
||
|
||
if (st) st.textContent = 'Wird gespeichert…';
|
||
fetchNui('setSignature', { address: addr, signature: el.value });
|
||
}
|
||
|
||
function onSignatureSaved(data) {
|
||
const st = document.getElementById('mail-sig-status');
|
||
if (!st) return;
|
||
st.textContent = (data && data.ok)
|
||
? 'Gespeichert.'
|
||
: '⚠ ' + ((data && data.error) || 'Speichern fehlgeschlagen.');
|
||
}
|
||
|
||
/* ── Postfächer freischalten (ic-web) ─────────────────── */
|
||
/* Ein Postfach ist ein Zugang mit Passwort. Wer die Daten kennt, bekommt es
|
||
hier dazu – dieselbe Prüfung wie bei der Anmeldung am Webhosting. */
|
||
function _addMailbox() {
|
||
if (typeof ICWebRender === 'undefined') {
|
||
return Desktop.showNotification('⚠ Postfachverwaltung nicht verfügbar.');
|
||
}
|
||
|
||
ICWebRender.modal('Postfach hinzufügen', (box, close) => {
|
||
const el = ICWebRender.el;
|
||
box.appendChild(el('div', 'icweb-modal-sub',
|
||
'Adresse und Passwort des Postfachs. Es bleibt geöffnet, bis du es '
|
||
+ 'entfernst oder den Server verlässt.'));
|
||
|
||
const mkField = (label, type, placeholder) => {
|
||
const row = el('div', 'wh-field');
|
||
row.appendChild(el('label', 'wh-label', label));
|
||
const input = el('input', 'icweb-input');
|
||
input.setAttribute('type', type);
|
||
if (placeholder) input.setAttribute('placeholder', placeholder);
|
||
row.appendChild(input);
|
||
box.appendChild(row);
|
||
return input;
|
||
};
|
||
|
||
const addr = mkField('Adresse', 'text', 'presse@weazel-news.ls');
|
||
const pass = mkField('Passwort', 'password', '');
|
||
const err = el('div', 'wh-login-error');
|
||
box.appendChild(err);
|
||
|
||
const row = el('div', 'icweb-modal-actions');
|
||
const cancel = el('button', 'icweb-btn-ghost', 'Abbrechen');
|
||
cancel.addEventListener('click', close);
|
||
|
||
const go = el('button', 'icweb-btn-primary', 'Hinzufügen');
|
||
const submit = async () => {
|
||
err.textContent = '';
|
||
if (!addr.value || !pass.value) {
|
||
err.textContent = 'Bitte Adresse und Passwort eingeben.';
|
||
return;
|
||
}
|
||
go.disabled = true;
|
||
const res = await ICWebNet.call('openMailbox', addr.value, pass.value);
|
||
go.disabled = false;
|
||
pass.value = '';
|
||
|
||
if (!res.ok) { err.textContent = res.error || 'Fehlgeschlagen.'; return; }
|
||
close();
|
||
refreshMailboxes();
|
||
};
|
||
go.addEventListener('click', submit);
|
||
[addr, pass].forEach(i =>
|
||
i.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); }));
|
||
|
||
row.append(cancel, go);
|
||
box.appendChild(row);
|
||
setTimeout(() => addr.focus(), 0);
|
||
});
|
||
}
|
||
|
||
function _removeMailbox(address) {
|
||
ICWebRender.confirmBox('Postfach entfernen',
|
||
'Postfach ' + address + ' entfernen?\n'
|
||
+ 'Die Nachrichten bleiben erhalten, du siehst sie nur nicht mehr.', async () => {
|
||
const res = await ICWebNet.call('closeMailbox', address);
|
||
if (!res.ok) return Desktop.showNotification('⚠ ' + (res.error || 'Fehlgeschlagen.'));
|
||
|
||
if (_activeAddr === address) _activeAddr = null;
|
||
refreshMailboxes();
|
||
}, { danger: true, okLabel: 'Entfernen' });
|
||
}
|
||
|
||
/* Liste der freigeschalteten Postfächer holen und die Mailboxliste neu
|
||
laden. Beides zusammen, damit die ✕-Knöpfe zur Anzeige passen. */
|
||
async function refreshMailboxes() {
|
||
if (typeof ICWebNet !== 'undefined') {
|
||
const res = await ICWebNet.call('listMailboxes');
|
||
_openBoxes = (res.ok && Array.isArray(res.data)) ? res.data : [];
|
||
}
|
||
fetchNui('getSharedMailboxes', {});
|
||
}
|
||
|
||
/* ── Compose ──────────────────────────────────────────── */
|
||
function renderCompose(prefillTo = '', prefillSubject = '') {
|
||
// Von-Dropdown: alle Postfächer aus denen der Spieler senden darf
|
||
const fromAddresses = [];
|
||
if (_personalAddr) fromAddresses.push({ address: _personalAddr, label: _personalAddr });
|
||
for (const mb of _mailboxes) {
|
||
if (mb.type !== 'personal') {
|
||
const lbl = mb.display_name ? `${mb.display_name} (${mb.address})` : mb.address;
|
||
fromAddresses.push({ address: mb.address, label: lbl });
|
||
}
|
||
}
|
||
const defaultFrom = _activeAddr || _personalAddr || '';
|
||
|
||
let fromField = '';
|
||
if (fromAddresses.length > 1) {
|
||
const opts = fromAddresses.map(o =>
|
||
`<option value="${esc(o.address)}" ${o.address === defaultFrom ? 'selected' : ''}>${esc(o.label)}</option>`
|
||
).join('');
|
||
fromField = `
|
||
<div class="form-group">
|
||
<label>Von</label>
|
||
<select id="mail-from" onchange="MailApp._fromChanged()">${opts}</select>
|
||
</div>`;
|
||
} else if (fromAddresses.length === 1) {
|
||
fromField = `<input type="hidden" id="mail-from" value="${esc(fromAddresses[0].address)}"/>`;
|
||
}
|
||
|
||
const contactOptions = _contacts.map(c =>
|
||
`<option value="${esc(c.email || c.name)}">${esc(c.name)}${c.email ? ' <' + esc(c.email) + '>' : ''}</option>`
|
||
).join('');
|
||
const datalist = _contacts.length ? `
|
||
<datalist id="mail-contacts-list">${contactOptions}</datalist>` : '';
|
||
|
||
return `
|
||
<div class="app-content fade-in" style="max-width:640px;padding:16px">
|
||
<h3 style="color:#fff;font-size:.9rem;margin-bottom:12px">✏ Neue Nachricht</h3>
|
||
${datalist}
|
||
${fromField}
|
||
<div class="form-group">
|
||
<label>An (Adresse oder Kontakt)</label>
|
||
<input id="mail-to" type="text" list="mail-contacts-list"
|
||
placeholder="name@domain… oder Kontakt wählen"
|
||
value="${esc(prefillTo)}"/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Betreff</label>
|
||
<input id="mail-subject" type="text" placeholder="Betreff…" maxlength="80"
|
||
value="${esc(prefillSubject)}"/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Nachricht</label>
|
||
<textarea id="mail-body" rows="9" placeholder="Nachricht schreiben…" maxlength="2000"
|
||
>${esc(applySignature('', defaultFrom))}</textarea>
|
||
</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn-primary" onclick="MailApp._sendMail()">📤 Senden</button>
|
||
<button class="btn-ghost" onclick="MailApp._setView('inbox')">Abbrechen</button>
|
||
</div>
|
||
<div id="mail-send-status" style="font-size:.72rem;margin-top:6px;color:var(--text-muted)"></div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Calendar ─────────────────────────────────────────── */
|
||
/* Kalender, in die diese Person eintragen darf: privat und jedes Postfach. */
|
||
function writableCalendars() {
|
||
const out = [{ address: '', label: '🔒 Privat' }];
|
||
for (const mb of _mailboxes) {
|
||
out.push({
|
||
address: mb.address,
|
||
label: '👥 ' + (mb.display_name || mb.address),
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/* Kurzes Schild am Termin, damit man sieht, wessen Kalender er gehört. */
|
||
function calBadge(e) {
|
||
if (e.visibility === 'public') return { text: '🌐 Öffentlich', title: e.address || '' };
|
||
if (e.address) {
|
||
const mb = _mailboxes.find(m => m.address === e.address);
|
||
return { text: '👥 ' + ((mb && mb.display_name) || e.address), title: e.address };
|
||
}
|
||
return { text: '🔒 Privat', title: '' };
|
||
}
|
||
|
||
function renderCalendar() {
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = now.getMonth();
|
||
|
||
const firstDay = new Date(year, month, 1).getDay();
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
const monthNames = ['Januar','Februar','März','April','Mai','Juni',
|
||
'Juli','August','September','Oktober','November','Dezember'];
|
||
|
||
let cells = '';
|
||
const dayHeaders = ['Mo','Di','Mi','Do','Fr','Sa','So'].map(d =>
|
||
`<div class="cal-header-cell">${d}</div>`).join('');
|
||
|
||
const offset = (firstDay + 6) % 7;
|
||
for (let i = 0; i < offset; i++) cells += `<div class="cal-cell cal-cell-empty"></div>`;
|
||
|
||
for (let d = 1; d <= daysInMonth; d++) {
|
||
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||
const dayEvents = _calendar.filter(e => e.start_at && e.start_at.startsWith(dateStr));
|
||
const dots = dayEvents.map(e =>
|
||
`<div class="cal-dot" style="background:${esc(e.color||'#00aaff')}" title="${esc(e.title)}"></div>`
|
||
).join('');
|
||
const isToday = (d === now.getDate());
|
||
cells += `
|
||
<div class="cal-cell ${isToday ? 'cal-today' : ''}" onclick="MailApp._calDayClick('${dateStr}')">
|
||
<span class="cal-day-num">${d}</span>
|
||
<div class="cal-dots">${dots}</div>
|
||
</div>`;
|
||
}
|
||
|
||
const upcomingItems = _calendar
|
||
.filter(e => new Date(e.start_at) >= new Date(year, month, 1))
|
||
.slice(0, 8)
|
||
.map(e => {
|
||
const badge = calBadge(e);
|
||
// Fremde öffentliche Termine sieht man, ändern darf sie nur, wer das
|
||
// zugehörige Postfach bedient.
|
||
const actions = e.can_edit ? `
|
||
<div style="margin-top:4px;display:flex;gap:4px">
|
||
<button class="btn-ghost btn-sm" onclick="MailApp._openCalEdit(${e.id})">✏</button>
|
||
<button class="btn-danger btn-sm" onclick="MailApp._deleteCalEvent(${e.id})">🗑</button>
|
||
</div>` : '';
|
||
return `
|
||
<div class="cal-event-item" style="border-left:3px solid ${esc(e.color||'#00aaff')}">
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<div style="font-weight:600;font-size:.78rem;flex:1">${esc(e.title)}</div>
|
||
<span class="cal-badge" title="${esc(badge.title)}">${esc(badge.text)}</span>
|
||
</div>
|
||
<div style="font-size:.7rem;color:var(--text-muted)">${fmtDate(e.start_at)}${e.end_at ? ' – ' + fmtDate(e.end_at) : ''}</div>
|
||
${e.description ? `<div style="font-size:.7rem;margin-top:2px;color:var(--text-muted)">${esc(e.description)}</div>` : ''}
|
||
${actions}
|
||
</div>`;
|
||
}).join('');
|
||
|
||
const editForm = _editEvent ? `
|
||
<div class="cal-edit-form fade-in">
|
||
<h4 style="font-size:.82rem;margin-bottom:8px;color:#fff">
|
||
${_editEvent.id ? 'Eintrag bearbeiten' : 'Neuer Eintrag'}
|
||
</h4>
|
||
<div class="form-group">
|
||
<label>Titel</label>
|
||
<input id="cal-title" type="text" value="${esc(_editEvent.title||'')}" maxlength="120"/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Beschreibung</label>
|
||
<input id="cal-desc" type="text" value="${esc(_editEvent.description||'')}" maxlength="500"/>
|
||
</div>
|
||
<div class="form-group" style="display:flex;gap:8px">
|
||
<div style="flex:1">
|
||
<label>Von</label>
|
||
<input id="cal-start" type="datetime-local" value="${fmtDateInput(_editEvent.start_at)}"/>
|
||
</div>
|
||
<div style="flex:1">
|
||
<label>Bis (optional)</label>
|
||
<input id="cal-end" type="datetime-local" value="${fmtDateInput(_editEvent.end_at)}"/>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Kalender</label>
|
||
<select id="cal-address" onchange="MailApp._calAddressChanged()">
|
||
${writableCalendars().map(c =>
|
||
`<option value="${esc(c.address)}" ${c.address === (_editEvent.address||'') ? 'selected' : ''}>${esc(c.label)}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
${(_editEvent.address || '') !== '' ? `
|
||
<div class="form-group">
|
||
<label>Sichtbar für</label>
|
||
<select id="cal-visibility">
|
||
<option value="shared" ${_editEvent.visibility !== 'public' ? 'selected' : ''}>Nur wer dieses Postfach bedient</option>
|
||
<option value="public" ${_editEvent.visibility === 'public' ? 'selected' : ''}>Alle (öffentlicher Termin)</option>
|
||
</select>
|
||
</div>` : `
|
||
<div style="font-size:.68rem;color:var(--text-dim);margin:-4px 0 8px">
|
||
Ein privater Termin ist nur für dich sichtbar.
|
||
</div>`}
|
||
<div class="form-group">
|
||
<label>Farbe</label>
|
||
<input id="cal-color" type="color" value="${_editEvent.color||'#00aaff'}" style="height:28px;cursor:pointer"/>
|
||
</div>
|
||
<div style="display:flex;gap:6px">
|
||
<button class="btn-primary" onclick="MailApp._saveCalEvent()">💾 Speichern</button>
|
||
<button class="btn-ghost" onclick="MailApp._cancelCalEdit()">Abbrechen</button>
|
||
</div>
|
||
</div>` : `
|
||
<button class="btn-primary" style="margin-top:8px;width:100%" onclick="MailApp._newCalEvent()">
|
||
+ Neuer Eintrag
|
||
</button>`;
|
||
|
||
return `
|
||
<div style="display:flex;height:100%;gap:0;overflow:hidden">
|
||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid var(--border)">
|
||
<div class="cal-month-header">📅 ${monthNames[month]} ${year}</div>
|
||
<div class="cal-grid">
|
||
${dayHeaders}
|
||
${cells}
|
||
</div>
|
||
</div>
|
||
<div style="width:280px;display:flex;flex-direction:column;overflow-y:auto;padding:10px;gap:8px">
|
||
${editForm}
|
||
<div class="sidebar-section" style="margin-top:8px">Kommende Termine</div>
|
||
${upcomingItems || '<div style="color:var(--text-dim);font-size:.75rem;padding:4px">Keine Termine</div>'}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Full layout refresh ──────────────────────────────── */
|
||
function _refresh() {
|
||
const body = WindowManager.getBody(WIN_ID);
|
||
if (!body) return;
|
||
|
||
const isSent = _view === 'sent';
|
||
const mails = currentMails();
|
||
const activeMail = mails.find(m => m.id === _active) || null;
|
||
|
||
let rightContent;
|
||
if (_view === 'compose') {
|
||
rightContent = renderCompose();
|
||
} else if (_view === 'signature') {
|
||
rightContent = renderSignature();
|
||
} else if (_view === 'calendar') {
|
||
rightContent = renderCalendar();
|
||
} else {
|
||
rightContent = `
|
||
<div style="display:flex;height:100%;overflow:hidden">
|
||
<div style="width:280px;border-right:1px solid var(--border);overflow-y:auto;flex-shrink:0">
|
||
${renderMailList(mails, isSent)}
|
||
</div>
|
||
<div style="flex:1;overflow-y:auto">
|
||
${renderMailDetail(activeMail, isSent)}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
body.innerHTML = `
|
||
<div class="app-layout" style="height:100%">
|
||
${renderSidebar()}
|
||
<div style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
||
${rightContent}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Mailbox actions ──────────────────────────────────── */
|
||
function _setMailbox(address) {
|
||
_activeAddr = address || null;
|
||
_view = 'inbox';
|
||
_active = null;
|
||
_editEvent = null;
|
||
|
||
// Jedes Postfach hat eigene Ordner – die des vorherigen passen nicht.
|
||
if (_foldersByBox[boxKey()] === undefined) {
|
||
fetchNui('getFolders', { address: boxKey() });
|
||
}
|
||
_refresh();
|
||
}
|
||
|
||
function initMailboxes(mailboxes, personalAddress, personalSignature) {
|
||
_mailboxes = mailboxes || [];
|
||
_personalAddr = personalAddress || '';
|
||
_personalSig = personalSignature || '';
|
||
const body = WindowManager.getBody(WIN_ID);
|
||
if (body) _refresh();
|
||
}
|
||
|
||
function onSharedMailboxes(mailboxes, personalAddress, personalSignature) {
|
||
_mailboxes = mailboxes || [];
|
||
_personalAddr = personalAddress || '';
|
||
_personalSig = personalSignature || '';
|
||
|
||
// Ein neu geöffnetes Postfach bringt seinen Kalender mit.
|
||
fetchNui('getCalendar', {});
|
||
|
||
const body = WindowManager.getBody(WIN_ID);
|
||
if (body) _refresh();
|
||
}
|
||
|
||
/* ── View actions ─────────────────────────────────────── */
|
||
function _setView(v) {
|
||
_view = v;
|
||
_active = null;
|
||
_editEvent = null;
|
||
if (v === 'sent' && !_sent.length) fetchNui('getSent', {});
|
||
if (v === 'calendar' && !_calendar.length) fetchNui('getCalendar', {});
|
||
if (v.startsWith('folder:')) {
|
||
const fid = parseInt(v.split(':')[1]);
|
||
if (!_folderMails[fid]) fetchNui('getFolderMails', { folderId: fid, address: boxKey() });
|
||
}
|
||
_refresh();
|
||
}
|
||
|
||
function _openMail(id) {
|
||
_active = id;
|
||
const isSent = _view === 'sent';
|
||
if (!isSent) {
|
||
fetchNui('readMail', { id });
|
||
const m = currentMails().find(m => m.id === id);
|
||
if (m) m.is_read = 1;
|
||
}
|
||
_refresh();
|
||
}
|
||
|
||
/* Absender gewechselt: alten Signaturblock ersetzen, Text behalten. */
|
||
function _fromChanged() {
|
||
const from = document.getElementById('mail-from')?.value || '';
|
||
const body = document.getElementById('mail-body');
|
||
if (!body) return;
|
||
|
||
const pos = body.selectionStart;
|
||
body.value = applySignature(body.value, from);
|
||
if (pos != null) { try { body.setSelectionRange(pos, pos); } catch (e) {} }
|
||
}
|
||
|
||
function _sendMail() {
|
||
const to = (document.getElementById('mail-to')?.value || '').trim();
|
||
const subject = (document.getElementById('mail-subject')?.value || '').trim();
|
||
const body = (document.getElementById('mail-body')?.value || '').trim();
|
||
const from = document.getElementById('mail-from')?.value || '';
|
||
const status = document.getElementById('mail-send-status');
|
||
if (!to || !subject || !body) {
|
||
if (status) status.textContent = 'Bitte alle Felder ausfüllen.';
|
||
return;
|
||
}
|
||
if (status) status.textContent = 'Wird gesendet…';
|
||
fetchNui('sendMail', { to, subject, body, fromMailbox: from });
|
||
}
|
||
|
||
function _reply() {
|
||
const mail = currentMails().find(m => m.id === _active);
|
||
if (!mail) return;
|
||
_view = 'compose';
|
||
_refresh();
|
||
setTimeout(() => {
|
||
const t = document.getElementById('mail-to');
|
||
const s = document.getElementById('mail-subject');
|
||
if (t) t.value = mail.from_identifier || '';
|
||
if (s) s.value = 'RE: ' + (mail.subject || '');
|
||
}, 0);
|
||
}
|
||
|
||
function _deleteMail(id) { fetchNui('deleteMail', { id }); }
|
||
|
||
function _moveMail(id, folderId) {
|
||
fetchNui('moveMail', {
|
||
id,
|
||
folderId: folderId === '' || folderId === 'null' ? null : folderId,
|
||
address: boxKey(),
|
||
});
|
||
}
|
||
|
||
function _promptNewFolder() {
|
||
// Kein prompt(): FiveMs CEF hat keinen Handler für die eingebauten
|
||
// JS-Dialoge, das NUI bliebe stehen.
|
||
const box = boxKey();
|
||
ICWebRender.promptText('Neuer Ordner', {
|
||
hint: box ? 'Der Ordner gehört zu ' + box + ' – alle, die dieses Postfach '
|
||
+ 'bedienen, sehen ihn.'
|
||
: 'Der Ordner gehört zu deinem persönlichen Postfach.',
|
||
placeholder: 'z. B. Anfragen',
|
||
okLabel: 'Anlegen',
|
||
}, (name) => {
|
||
fetchNui('createFolder', { name, address: box });
|
||
});
|
||
}
|
||
|
||
function _deleteFolder(id) {
|
||
const box = boxKey();
|
||
ICWebRender.confirmBox('Ordner löschen',
|
||
'E-Mails kommen zurück in den Posteingang.'
|
||
+ (box ? '\nDer Ordner verschwindet für alle, die ' + box + ' bedienen.' : ''),
|
||
() => fetchNui('deleteFolder', { id, address: box }),
|
||
{ danger: true, okLabel: 'Löschen' });
|
||
}
|
||
|
||
function _saveAsContact(emailOrId) {
|
||
AddressBookApp.openAddNew({ email: emailOrId });
|
||
}
|
||
|
||
/* ── Calendar actions ─────────────────────────────────── */
|
||
/* Vorbelegung: der Kalender des gerade gewählten Postfachs. Wer im
|
||
Firmenpostfach arbeitet, trägt meist auch dort ein. */
|
||
function newCalDraft(start) {
|
||
return {
|
||
title: '', description: '', start_at: start || '', end_at: '',
|
||
color: '#00aaff', address: boxKey(),
|
||
visibility: boxKey() ? 'shared' : 'private',
|
||
};
|
||
}
|
||
|
||
function _newCalEvent(prefillDate) {
|
||
_editEvent = newCalDraft(prefillDate);
|
||
_refresh();
|
||
}
|
||
|
||
function _calDayClick(dateStr) {
|
||
if (_view !== 'calendar') return;
|
||
_editEvent = newCalDraft(dateStr + 'T08:00');
|
||
_refresh();
|
||
}
|
||
|
||
/* Kalender gewechselt: Formular neu zeichnen, damit die Sichtbarkeitsauswahl
|
||
erscheint bzw. verschwindet. Eingaben bleiben erhalten. */
|
||
function _calAddressChanged() {
|
||
if (!_editEvent) return;
|
||
readCalForm();
|
||
const sel = document.getElementById('cal-address');
|
||
_editEvent.address = sel ? sel.value : '';
|
||
if (!_editEvent.address) _editEvent.visibility = 'private';
|
||
else if (_editEvent.visibility === 'private') _editEvent.visibility = 'shared';
|
||
_refresh();
|
||
}
|
||
|
||
/* Aktuelle Formularwerte in den Entwurf übernehmen. */
|
||
function readCalForm() {
|
||
if (!_editEvent) return;
|
||
const v = (id) => document.getElementById(id)?.value;
|
||
_editEvent.title = v('cal-title') ?? _editEvent.title;
|
||
_editEvent.description = v('cal-desc') ?? _editEvent.description;
|
||
_editEvent.start_at = v('cal-start') ?? _editEvent.start_at;
|
||
_editEvent.end_at = v('cal-end') ?? _editEvent.end_at;
|
||
_editEvent.color = v('cal-color') ?? _editEvent.color;
|
||
const vis = v('cal-visibility');
|
||
if (vis) _editEvent.visibility = vis;
|
||
}
|
||
|
||
function _openCalEdit(id) {
|
||
const ev = _calendar.find(e => e.id === id);
|
||
if (!ev) return;
|
||
if (!ev.can_edit) {
|
||
return Desktop.showNotification('⚠ Diesen Termin darfst du nicht ändern.');
|
||
}
|
||
_editEvent = { ...ev, address: ev.address || '' };
|
||
_refresh();
|
||
}
|
||
|
||
function _deleteCalEvent(id) {
|
||
const ev = _calendar.find(e => e.id === id);
|
||
if (!ev || !ev.can_edit) {
|
||
return Desktop.showNotification('⚠ Diesen Termin darfst du nicht löschen.');
|
||
}
|
||
|
||
const badge = calBadge(ev);
|
||
ICWebRender.confirmBox('Termin löschen',
|
||
'„' + ev.title + '" wirklich löschen?'
|
||
+ (ev.address ? '\nDer Termin verschwindet für alle in ' + badge.text + '.' : ''),
|
||
() => fetchNui('deleteCalendarEvent', { id }),
|
||
{ danger: true, okLabel: 'Löschen' });
|
||
}
|
||
|
||
function _cancelCalEdit() {
|
||
_editEvent = null;
|
||
_refresh();
|
||
}
|
||
|
||
function _saveCalEvent() {
|
||
const title = document.getElementById('cal-title')?.value.trim();
|
||
const desc = document.getElementById('cal-desc')?.value.trim();
|
||
const start = document.getElementById('cal-start')?.value;
|
||
const end = document.getElementById('cal-end')?.value;
|
||
const color = document.getElementById('cal-color')?.value || '#00aaff';
|
||
const address = document.getElementById('cal-address')?.value || '';
|
||
const visibility = address
|
||
? (document.getElementById('cal-visibility')?.value || 'shared')
|
||
: 'private';
|
||
if (!title || !start) return;
|
||
const payload = { title, description: desc, start_at: start, end_at: end || null,
|
||
color, address, visibility };
|
||
if (_editEvent && _editEvent.id) {
|
||
fetchNui('updateCalendarEvent', { id: _editEvent.id, ...payload });
|
||
} else {
|
||
fetchNui('addCalendarEvent', payload);
|
||
}
|
||
_editEvent = null;
|
||
}
|
||
|
||
/* ── NUI events ───────────────────────────────────────── */
|
||
function onInbox(payload) {
|
||
_inbox = (payload && payload.mails) ? payload.mails : (Array.isArray(payload) ? payload : []);
|
||
_foldersByBox[''] = (payload && payload.folders) ? payload.folders : [];
|
||
if (_view === 'inbox' || _view.startsWith('folder:')) _refresh();
|
||
}
|
||
|
||
function onMailContent(mail) {
|
||
const idx = _inbox.findIndex(m => m.id === mail.id);
|
||
if (idx !== -1) _inbox[idx] = { ..._inbox[idx], ...mail };
|
||
if (_active === mail.id && _view === 'inbox') _refresh();
|
||
}
|
||
|
||
function onMailSent() {
|
||
const status = document.getElementById('mail-send-status');
|
||
if (status) status.textContent = 'Gesendet!';
|
||
_sent = [];
|
||
setTimeout(() => _setView('inbox'), 800);
|
||
fetchNui('getInbox', {});
|
||
}
|
||
|
||
function onMailDeleted(id) {
|
||
_inbox = _inbox.filter(m => m.id !== id);
|
||
for (const fid in _folderMails) _folderMails[fid] = _folderMails[fid].filter(m => m.id !== id);
|
||
if (_active === id) _active = null;
|
||
_refresh();
|
||
}
|
||
|
||
function onSetSent(rows) {
|
||
// Duplikate entfernen (Rundmails an mehrere Empfänger)
|
||
const seen = new Set();
|
||
_sent = (rows || []).filter(m => {
|
||
const key = `${m.from_identifier}|${m.subject}|${m.sent_at}`;
|
||
if (seen.has(key)) return false;
|
||
seen.add(key);
|
||
return true;
|
||
});
|
||
if (_view === 'sent') _refresh();
|
||
}
|
||
|
||
function onFolderCreated(folder) {
|
||
const key = (folder && folder.address) || '';
|
||
_foldersByBox[key] = (_foldersByBox[key] || []).concat([folder]);
|
||
_folderMails[folder.id] = [];
|
||
_refresh();
|
||
}
|
||
|
||
function onFolderDeleted(folderId, address) {
|
||
const key = address || '';
|
||
_foldersByBox[key] = (_foldersByBox[key] || []).filter(f => f.id !== folderId);
|
||
delete _folderMails[folderId];
|
||
_inbox = [];
|
||
fetchNui('getInbox', {});
|
||
if (_view === 'folder:' + folderId) _view = 'inbox';
|
||
_refresh();
|
||
}
|
||
|
||
/* Ordnerliste eines Postfachs vom Server. Kommt beim Wechsel und wenn
|
||
jemand anderes im geteilten Postfach einen Ordner anlegt oder löscht. */
|
||
function onFoldersData(address, folders) {
|
||
_foldersByBox[address || ''] = folders || [];
|
||
_refresh();
|
||
}
|
||
|
||
function onFolderMailsData(folderId, rows) {
|
||
_folderMails[folderId] = rows || [];
|
||
if (_view === 'folder:' + folderId) _refresh();
|
||
}
|
||
|
||
function onMailMoved(id, folderId) {
|
||
_inbox = _inbox.filter(m => m.id !== id);
|
||
for (const fid in _folderMails) _folderMails[fid] = _folderMails[fid].filter(m => m.id !== id);
|
||
if (folderId) fetchNui('getFolderMails', { folderId });
|
||
if (_active === id) _active = null;
|
||
_refresh();
|
||
}
|
||
|
||
function onCalendarData(rows) { _calendar = rows || []; if (_view === 'calendar') _refresh(); }
|
||
function onCalendarEventAdded(ev) { _calendar.push(ev); _calendar.sort((a,b)=>new Date(a.start_at)-new Date(b.start_at)); if (_view==='calendar') _refresh(); }
|
||
function onCalendarEventUpdated(ev) { const i=_calendar.findIndex(e=>e.id===ev.id); if(i!==-1)_calendar[i]=ev; else _calendar.push(ev); _calendar.sort((a,b)=>new Date(a.start_at)-new Date(b.start_at)); if(_view==='calendar') _refresh(); }
|
||
function onCalendarEventDeleted(id) { _calendar=_calendar.filter(e=>e.id!==id); if(_view==='calendar') _refresh(); }
|
||
|
||
function setContacts(contacts) { _contacts = contacts || []; }
|
||
|
||
/* ── Setup wizard ─────────────────────────────────────── */
|
||
function _showSetupWizard(realms) {
|
||
const body = WindowManager.getBody(WIN_ID);
|
||
if (!body) return;
|
||
const defaultRealm = (realms && realms.length > 0) ? realms[0].realm : 'mail.ls';
|
||
body.innerHTML = `
|
||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||
height:100%;gap:18px;padding:40px 32px;text-align:center;">
|
||
<div style="font-size:3rem">📧</div>
|
||
<div style="font-size:1.05rem;font-weight:700;color:#fff">E-Mail-Adresse einrichten</div>
|
||
<div style="font-size:0.82rem;color:var(--text-muted);line-height:1.6;max-width:340px">
|
||
Du hast noch keine persönliche E-Mail-Adresse.<br>
|
||
Wähle einen Benutzernamen für <strong style="color:#ccc">@${defaultRealm}</strong>.
|
||
</div>
|
||
<div style="display:flex;align-items:center;background:#1a1a1a;border:1px solid var(--border);
|
||
border-radius:8px;overflow:hidden;width:100%;max-width:360px">
|
||
<input id="mail-setup-input" type="text" maxlength="40" placeholder="benutzername"
|
||
style="flex:1;background:transparent;border:none;color:#fff;padding:11px 14px;
|
||
font-size:0.9rem;outline:none;"
|
||
oninput="MailApp._setupPreview('${defaultRealm}')">
|
||
<span style="padding:0 14px;color:#555;font-size:0.85rem">@${defaultRealm}</span>
|
||
</div>
|
||
<div id="mail-setup-preview" style="font-size:0.75rem;color:#555;min-height:14px"></div>
|
||
<button onclick="MailApp._submitSetup('${defaultRealm}')"
|
||
style="background:var(--accent,#3b82f6);color:#fff;border:none;border-radius:8px;
|
||
padding:11px 36px;font-size:0.88rem;font-weight:700;cursor:pointer">
|
||
Adresse erstellen
|
||
</button>
|
||
<div id="mail-setup-error" style="color:#f44;font-size:0.78rem;min-height:14px"></div>
|
||
</div>`;
|
||
}
|
||
|
||
function _setupPreview(realm) {
|
||
const input = document.getElementById('mail-setup-input');
|
||
const preview = document.getElementById('mail-setup-preview');
|
||
if (!input || !preview) return;
|
||
const val = input.value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, '');
|
||
if (val.length >= 2) {
|
||
preview.textContent = '✓ ' + val + '@' + realm;
|
||
preview.style.color = '#4caf50';
|
||
} else {
|
||
preview.textContent = val.length ? 'Mindestens 2 Zeichen' : '';
|
||
preview.style.color = '#888';
|
||
}
|
||
}
|
||
|
||
function _submitSetup(realm) {
|
||
const input = document.getElementById('mail-setup-input');
|
||
const errEl = document.getElementById('mail-setup-error');
|
||
if (!input) return;
|
||
const username = input.value.trim();
|
||
if (username.length < 2) {
|
||
if (errEl) errEl.textContent = 'Bitte mindestens 2 Zeichen eingeben';
|
||
return;
|
||
}
|
||
if (errEl) errEl.textContent = '';
|
||
input.disabled = true;
|
||
fetchNui('createMailAddress', { username });
|
||
}
|
||
|
||
function onMailAddressStatus(data) {
|
||
if (data.hasAddress) {
|
||
// Adresse vorhanden → normal laden
|
||
fetchNui('getInbox', {});
|
||
fetchNui('getSharedMailboxes', {});
|
||
_refresh();
|
||
} else {
|
||
_showSetupWizard(data.realms || []);
|
||
}
|
||
}
|
||
|
||
function onMailCreateResult(data) {
|
||
if (data.success) {
|
||
const body = WindowManager.getBody(WIN_ID);
|
||
if (body) {
|
||
body.innerHTML = `
|
||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||
height:100%;gap:14px;text-align:center;padding:40px">
|
||
<div style="font-size:2.5rem">✅</div>
|
||
<div style="font-size:1rem;font-weight:700;color:#fff">Adresse erstellt!</div>
|
||
<div style="font-family:monospace;font-size:0.95rem;color:var(--accent,#3b82f6);
|
||
background:#0a1020;border:1px solid #1a2a40;padding:8px 20px;border-radius:6px">
|
||
${data.address}
|
||
</div>
|
||
<div style="font-size:0.78rem;color:var(--text-muted)">Postfach wird geladen…</div>
|
||
</div>`;
|
||
}
|
||
_personalAddr = data.address;
|
||
setTimeout(() => {
|
||
fetchNui('getInbox', {});
|
||
fetchNui('getSharedMailboxes', {});
|
||
_refresh();
|
||
}, 1500);
|
||
} else {
|
||
const errEl = document.getElementById('mail-setup-error');
|
||
const input = document.getElementById('mail-setup-input');
|
||
if (errEl) errEl.textContent = data.error || 'Fehler beim Erstellen';
|
||
if (input) input.disabled = false;
|
||
}
|
||
}
|
||
|
||
/* ── Open window ──────────────────────────────────────── */
|
||
function open() {
|
||
const created = WindowManager.create({
|
||
id: WIN_ID,
|
||
title: 'Mail',
|
||
icon: '✉',
|
||
width: 920,
|
||
height: 580,
|
||
content: '',
|
||
});
|
||
if (created) {
|
||
_view = 'inbox';
|
||
_active = null;
|
||
_activeAddr = null;
|
||
// Erst prüfen ob eine Mailadresse existiert
|
||
fetchNui('checkMailAddress', {});
|
||
}
|
||
}
|
||
|
||
return {
|
||
open, initMailboxes, onSharedMailboxes,
|
||
onMailAddressStatus, onMailCreateResult,
|
||
_setupPreview, _submitSetup,
|
||
_setMailbox, _setView, _openMail, _sendMail, _reply, _deleteMail,
|
||
_addMailbox, _removeMailbox, _saveSignature, _sigBoxChanged, _fromChanged,
|
||
onSignatureSaved, refreshMailboxes,
|
||
_moveMail, _promptNewFolder, _deleteFolder, _saveAsContact,
|
||
_newCalEvent, _calDayClick, _openCalEdit, _deleteCalEvent, _cancelCalEdit, _saveCalEvent,
|
||
_calAddressChanged,
|
||
onInbox, onMailContent, onMailSent, onMailDeleted,
|
||
onSetSent, onFolderCreated, onFolderDeleted, onFoldersData, onFolderMailsData, onMailMoved,
|
||
onCalendarData, onCalendarEventAdded, onCalendarEventUpdated, onCalendarEventDeleted,
|
||
setContacts,
|
||
};
|
||
})();
|