308 lines
11 KiB
JavaScript
308 lines
11 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
|||
|
|
}
|
|||
|
|
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,
|
|||
|
|
};
|
|||
|
|
})();
|