pc-live/nui/js/apps/addressbook.js
Bjoern Flessing be502d2834 IC-Computer: Schreibtisch, Fenster und Apps
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.
2026-08-09 12:02:11 +00:00

307 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* pc-live | Adressbuch App
* Kontakte verwalten, an Mail-App weitergeben, Kontaktkarte per Mail senden.
*/
const AddressBookApp = (() => {
const WIN_ID = 'app-addressbook';
let _contacts = [];
let _active = null; // contact id
let _editing = false;
let _editData = null;
/* ── Helpers ──────────────────────────────────────────── */
function esc(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function fmtDate(d) {
if (!d) return '';
const dt = new Date(d);
return isNaN(dt) ? '' : dt.toLocaleDateString('de-DE');
}
function initials(name) {
return (name || '?').split(' ').map(p => p[0] || '').slice(0,2).join('').toUpperCase();
}
function avatarColor(name) {
const colors = ['#3b82f6','#8b5cf6','#ec4899','#f59e0b','#10b981','#06b6d4','#ef4444'];
let h = 0;
for (const c of (name||'?')) h = (h * 31 + c.charCodeAt(0)) & 0xffffffff;
return colors[Math.abs(h) % colors.length];
}
/* ── Sidebar list ─────────────────────────────────────── */
function renderList() {
if (!_contacts.length)
return `<div class="empty-state" style="padding:24px"><div class="empty-icon">👤</div><p>Keine Kontakte</p></div>`;
return `<div class="mail-list">` + _contacts.map(c => `
<div class="mail-item ${_active === c.id ? 'active' : ''}" onclick="AddressBookApp._select(${c.id})">
<div style="display:flex;align-items:center;gap:8px">
<div class="contact-avatar" style="background:${avatarColor(c.name)}">${esc(initials(c.name))}</div>
<div style="flex:1;overflow:hidden">
<div class="mail-from" style="font-weight:600">${esc(c.name)}</div>
${c.email ? `<div class="mail-date">${esc(c.email)}</div>` : ''}
${c.phone ? `<div class="mail-date">${esc(c.phone)}</div>` : ''}
</div>
</div>
</div>`).join('') + `</div>`;
}
/* ── Detail / Edit ────────────────────────────────────── */
function renderDetail() {
if (_editing) {
const d = _editData || {};
return `
<div class="app-content fade-in" style="padding:20px;max-width:480px">
<h3 style="color:#fff;font-size:.9rem;margin-bottom:14px">
${d.id ? '✏ Kontakt bearbeiten' : ' Neuer Kontakt'}
</h3>
<div class="form-group">
<label>Name *</label>
<input id="ab-name" type="text" value="${esc(d.name||'')}" maxlength="100" placeholder="Max Mustermann"/>
</div>
<div class="form-group">
<label>E-Mail / Identifier</label>
<input id="ab-email" type="text" value="${esc(d.email||'')}" maxlength="120" placeholder="license:abc… oder email@…"/>
</div>
<div class="form-group">
<label>Telefon</label>
<input id="ab-phone" type="text" value="${esc(d.phone||'')}" maxlength="30" placeholder="+49 123 456789"/>
</div>
<div class="form-group">
<label>Notizen</label>
<textarea id="ab-notes" rows="4" maxlength="500" placeholder="Notizen…">${esc(d.notes||'')}</textarea>
</div>
<div style="display:flex;gap:8px">
<button class="btn-primary" onclick="AddressBookApp._save()">💾 Speichern</button>
<button class="btn-ghost" onclick="AddressBookApp._cancelEdit()">Abbrechen</button>
</div>
<div id="ab-status" style="font-size:.72rem;margin-top:6px;color:var(--error)"></div>
</div>`;
}
const c = _contacts.find(x => x.id === _active);
if (!c)
return `<div class="empty-state"><div class="empty-icon">👤</div><p>Kontakt auswählen</p></div>`;
const bg = avatarColor(c.name);
return `
<div class="mail-detail fade-in" style="padding:24px">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<div class="contact-avatar contact-avatar-lg" style="background:${bg}">${esc(initials(c.name))}</div>
<div>
<div style="font-size:1.1rem;font-weight:700;color:#fff">${esc(c.name)}</div>
<div style="font-size:.75rem;color:var(--text-muted)">Hinzugefügt: ${fmtDate(c.created_at)}</div>
</div>
</div>
<div class="contact-field-group">
${c.email ? `
<div class="contact-field">
<span class="contact-field-label">✉ E-Mail / ID</span>
<span class="contact-field-value">${esc(c.email)}</span>
</div>` : ''}
${c.phone ? `
<div class="contact-field">
<span class="contact-field-label">📞 Telefon</span>
<span class="contact-field-value">${esc(c.phone)}</span>
</div>` : ''}
${c.notes ? `
<div class="contact-field">
<span class="contact-field-label">📝 Notizen</span>
<span class="contact-field-value" style="white-space:pre-wrap">${esc(c.notes)}</span>
</div>` : ''}
</div>
<div style="display:flex;gap:8px;margin-top:20px;flex-wrap:wrap">
${c.email ? `
<button class="btn-primary" onclick="AddressBookApp._mailTo('${esc(c.email)}')">
✉ Mail schreiben
</button>
<button class="btn-ghost" onclick="AddressBookApp._forwardContact(${c.id})">
📤 Kontakt weiterleiten
</button>` : ''}
<button class="btn-ghost" onclick="AddressBookApp._startEdit(${c.id})">✏ Bearbeiten</button>
<button class="btn-danger" onclick="AddressBookApp._delete(${c.id})">🗑 Löschen</button>
</div>
</div>`;
}
/* ── Full refresh ─────────────────────────────────────── */
function _refresh() {
const body = WindowManager.getBody(WIN_ID);
if (!body) return;
// Push contacts into mail app for autocomplete
MailApp.setContacts(_contacts);
body.innerHTML = `
<div class="app-layout" style="height:100%">
<div class="app-sidebar" style="min-width:220px;max-width:250px">
<div class="sidebar-section">Kontakte (${_contacts.length})</div>
<div style="padding:6px 10px">
<button class="btn-primary" style="width:100%;font-size:.75rem"
onclick="AddressBookApp._startEdit(null)"> Neuer Kontakt</button>
</div>
<div style="overflow-y:auto;flex:1">
${renderList()}
</div>
</div>
<div style="flex:1;overflow-y:auto">
${renderDetail()}
</div>
</div>`;
}
/* ── Actions ──────────────────────────────────────────── */
function _select(id) {
_active = id;
_editing = false;
_editData = null;
_refresh();
}
function _startEdit(id) {
_editing = true;
_editData = id ? { ...(_contacts.find(c => c.id === id) || {}) } : {};
_active = id;
_refresh();
}
function _cancelEdit() {
_editing = false;
_editData = null;
_refresh();
}
function _save() {
const name = document.getElementById('ab-name')?.value.trim();
const email = document.getElementById('ab-email')?.value.trim();
const phone = document.getElementById('ab-phone')?.value.trim();
const notes = document.getElementById('ab-notes')?.value.trim();
const status = document.getElementById('ab-status');
if (!name) { if (status) status.textContent = 'Name ist Pflichtfeld.'; return; }
const payload = { name, email, phone, notes };
if (_editData && _editData.id) {
fetchNui('updateContact', { id: _editData.id, ...payload });
} else {
fetchNui('addContact', payload);
}
_editing = false;
_editData = null;
}
function _delete(id) {
// Kein confirm(): FiveMs CEF hat keinen Handler für die eingebauten
// JS-Dialoge der Aufruf öffnet nichts und lässt das NUI stehen.
ICWebRender.confirmBox('Kontakt löschen', 'Kontakt wirklich löschen?',
() => fetchNui('deleteContact', { id }),
{ danger: true, okLabel: 'Löschen' });
}
function _mailTo(emailOrId) {
// Open mail app in compose mode with prefilled To
MailApp.open();
setTimeout(() => {
MailApp._setView('compose');
setTimeout(() => {
const t = document.getElementById('mail-to');
if (t) { t.value = emailOrId; t.focus(); }
}, 50);
}, 50);
}
function _forwardContact(id) {
const c = _contacts.find(x => x.id === id);
if (!c) return;
const body = [
`── Kontaktkarte ──`,
`Name: ${c.name}`,
c.email ? `E-Mail: ${c.email}` : null,
c.phone ? `Telefon: ${c.phone}` : null,
c.notes ? `Notizen: ${c.notes}` : null,
].filter(Boolean).join('\n');
MailApp.open();
setTimeout(() => {
MailApp._setView('compose');
setTimeout(() => {
const s = document.getElementById('mail-subject');
const b = document.getElementById('mail-body');
if (s) s.value = `Kontakt: ${c.name}`;
if (b) b.value = body;
document.getElementById('mail-to')?.focus();
}, 50);
}, 50);
}
/* ── Called from MailApp ("Als Kontakt speichern") ─────── */
function openAddNew(prefill) {
open();
setTimeout(() => {
_editing = true;
_editData = prefill || {};
_active = null;
_refresh();
}, 100);
}
/* ── NUI events ───────────────────────────────────────── */
function onContactsData(rows) {
_contacts = rows || [];
MailApp.setContacts(_contacts);
if (WindowManager.getBody(WIN_ID)) _refresh();
}
function onContactAdded(c) {
_contacts.push(c);
_contacts.sort((a,b) => a.name.localeCompare(b.name));
_active = c.id;
_editing = false;
MailApp.setContacts(_contacts);
_refresh();
}
function onContactUpdated(c) {
const idx = _contacts.findIndex(x => x.id === c.id);
if (idx !== -1) _contacts[idx] = c; else _contacts.push(c);
_contacts.sort((a,b) => a.name.localeCompare(b.name));
_active = c.id;
_editing = false;
MailApp.setContacts(_contacts);
_refresh();
}
function onContactDeleted(id) {
_contacts = _contacts.filter(c => c.id !== id);
if (_active === id) _active = null;
MailApp.setContacts(_contacts);
_refresh();
}
/* ── Open window ──────────────────────────────────────── */
function open() {
const created = WindowManager.create({
id: WIN_ID,
title: 'Adressbuch',
icon: '👤',
width: 720,
height: 500,
content: '',
});
if (created) {
_active = null;
_editing = false;
fetchNui('getContacts', {});
_refresh();
}
}
return {
open, openAddNew,
_select, _startEdit, _cancelEdit, _save, _delete,
_mailTo, _forwardContact,
onContactsData, onContactAdded, onContactUpdated, onContactDeleted,
};
})();