Files
crm-system/app/webui/contacts-detail.html
T

269 lines
14 KiB
HTML
Raw Normal View History

2026-06-03 23:52:17 +00:00
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CRM Contact Detail</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/css/app.css" />
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<script type="module" src="/js/store.js"></script>
<script type="module" src="/js/components/notifications.js"></script>
</head>
<body class="min-h-screen bg-slate-50">
<div x-data="toastContainer()" class="crm-toast-container" x-cloak>
<template x-for="n in items" :key="n.id">
<div class="crm-toast" :class="'toast-' + n.type"
@click="dismiss(n.id)" x-text="n.message"></div>
</template>
</div>
<div x-data="{ init: () => Alpine.store('auth').init() }" x-init="init()" class="flex min-h-screen">
<aside class="crm-sidebar">
<div class="px-6 py-4 text-white text-lg font-bold border-b border-slate-700">
<a href="/app.html" class="!text-white !border-0">CRM</a>
</div>
<nav class="mt-2">
<a href="/app.html">Dashboard</a>
<a href="/accounts.html">Accounts</a>
<a href="/contacts.html" class="active">Contacts</a>
<a href="/pipeline.html">Pipeline</a>
<a href="/activities.html">Activities</a>
<a href="/settings-profile.html">Profil</a>
<a href="/settings-users.html" x-show="$store.auth.isAdmin">Users (Admin)</a>
<a href="/settings-org.html" x-show="$store.auth.isAdmin">Org (Admin)</a>
</nav>
</aside>
<div class="flex-1 flex flex-col">
<header class="bg-white border-b border-slate-200 px-6 py-3 flex items-center justify-between">
<h1 class="text-xl font-semibold text-slate-800">Contact-Detail</h1>
<div class="flex items-center gap-3 text-sm">
<span class="text-slate-600">
<strong class="text-slate-900"
x-text="$store.auth.user ? $store.auth.user.name : '…'"></strong>
</span>
<button class="btn-secondary" @click="$store.auth.logout()">Logout</button>
</div>
</header>
<main class="flex-1 p-6" x-data="contactDetail()" x-init="init()">
<div x-show="loading" class="crm-card text-center py-12">
<div class="crm-spinner mx-auto"></div>
<p class="text-slate-500 mt-2">Lade Contact …</p>
</div>
<div x-show="!loading && contact">
<div class="crm-card mb-4">
<div class="flex items-start justify-between flex-wrap gap-2">
<div>
<h2 class="text-2xl font-bold text-slate-900"
x-text="(contact?.first_name || '') + ' ' + (contact?.last_name || '')"></h2>
<p class="text-slate-500 text-sm mt-1">
<span x-text="contact?.email || ''"></span>
<span class="mx-2">·</span>
<span x-text="contact?.phone || ''"></span>
<span class="mx-2">·</span>
<span>Account #<span x-text="contact?.account_id || ''"></span></span>
</p>
</div>
<div class="flex gap-2">
<button @click="openEdit()" class="btn-secondary">Bearbeiten</button>
<button @click="confirmDelete()" class="btn-danger">Löschen</button>
</div>
</div>
</div>
<div class="flex border-b border-slate-200 mb-4">
<button @click="tab='info'" :class="tabBtn('info')">Info</button>
<button @click="loadActivities()" :class="tabBtn('activities')">Activities</button>
<button @click="loadNotes()" :class="tabBtn('notes')">Notes</button>
</div>
<div x-show="tab === 'info'" class="crm-card">
<dl class="grid grid-cols-2 gap-4 text-sm">
<div><dt class="text-slate-500">Vorname</dt><dd class="font-medium" x-text="contact?.first_name"></dd></div>
<div><dt class="text-slate-500">Nachname</dt><dd class="font-medium" x-text="contact?.last_name"></dd></div>
<div><dt class="text-slate-500">E-Mail</dt><dd x-text="contact?.email || ''"></dd></div>
<div><dt class="text-slate-500">Telefon</dt><dd x-text="contact?.phone || ''"></dd></div>
<div><dt class="text-slate-500">Account</dt><dd x-text="contact?.account_id || ''"></dd></div>
<div><dt class="text-slate-500">Owner</dt><dd x-text="contact?.owner_id"></dd></div>
</dl>
</div>
<div x-show="tab === 'activities'" class="crm-card">
<div x-show="loadingSub" class="text-center py-4"><div class="crm-spinner mx-auto"></div></div>
<ul class="divide-y divide-slate-100" x-show="!loadingSub">
<template x-for="a in subLists.activities" :key="a.id">
<li class="py-2 flex items-start gap-3">
<span class="crm-badge badge-blue" x-text="a.type"></span>
<div class="flex-1">
<p class="text-sm font-medium" x-text="a.subject"></p>
<p class="text-xs text-slate-500" x-text="a.body || ''"></p>
</div>
</li>
</template>
<li x-show="!loadingSub && subLists.activities.length === 0"
class="text-center text-slate-500 py-3 text-sm">Keine Activities.</li>
</ul>
</div>
<div x-show="tab === 'notes'" class="crm-card">
<div x-show="loadingSub" class="text-center py-4"><div class="crm-spinner mx-auto"></div></div>
<ul class="space-y-2" x-show="!loadingSub">
<template x-for="n in subLists.notes" :key="n.id">
<li class="border-l-4 border-blue-500 pl-3 py-1">
<p class="text-sm" x-text="n.body"></p>
<p class="text-xs text-slate-400" x-text="formatDateTime(n.created_at)"></p>
</li>
</template>
<li x-show="!loadingSub && subLists.notes.length === 0"
class="text-center text-slate-500 py-3 text-sm">Keine Notes.</li>
</ul>
</div>
</div>
<!-- Edit modal -->
<div x-show="showEdit" x-cloak class="crm-modal-backdrop" @click.self="closeEdit()">
<div class="crm-modal p-6">
<h2 class="text-lg font-semibold mb-4">Contact bearbeiten</h2>
<form @submit.prevent="submitEdit()">
<div class="grid grid-cols-2 gap-3 mb-3">
<div>
<label class="crm-label">Vorname *</label>
<input type="text" required x-model="editForm.first_name" class="crm-input" />
</div>
<div>
<label class="crm-label">Nachname *</label>
<input type="text" required x-model="editForm.last_name" class="crm-input" />
</div>
</div>
<div class="mb-3">
<label class="crm-label">E-Mail</label>
<input type="email" x-model="editForm.email" class="crm-input" />
</div>
<div class="mb-3">
<label class="crm-label">Telefon</label>
<input type="text" maxlength="64" x-model="editForm.phone" class="crm-input" />
</div>
<div class="mb-3">
<label class="crm-label">Account-ID</label>
<input type="number" min="1" x-model="editForm.account_id" class="crm-input" />
</div>
<div x-show="editError" class="mb-3 p-2 bg-red-50 text-red-700 text-sm rounded"
x-text="editError"></div>
<div class="flex gap-2 justify-end">
<button type="button" @click="closeEdit()" class="btn-secondary">Abbrechen</button>
<button type="submit" :disabled="saving" class="btn-primary"
x-text="saving ? 'Speichere …' : 'Speichern'"></button>
</div>
</form>
</div>
</div>
</main>
</div>
</div>
<script>
function contactDetail() {
return {
id: null,
contact: null,
loading: true,
tab: 'info',
subLists: { activities: [], notes: [] },
loadingSub: false,
showEdit: false,
editForm: { first_name: '', last_name: '', email: '', phone: '', account_id: '' },
editError: '',
saving: false,
async init() {
const params = new URLSearchParams(window.location.search);
this.id = params.get('id');
if (!this.id) { window.location.href = '/contacts.html'; return; }
await this.load();
},
async load() {
this.loading = true;
try {
const { api } = await import('/js/api.js');
this.contact = await api.get(`/contacts/${this.id}`);
this.editForm = {
first_name: this.contact.first_name || '',
last_name: this.contact.last_name || '',
email: this.contact.email || '',
phone: this.contact.phone || '',
account_id: this.contact.account_id || '',
};
} catch (err) {
Alpine.store('notifications').error(err.message || 'Fehler.');
} finally { this.loading = false; }
},
async loadActivities() {
this.tab = 'activities';
this.loadingSub = true;
try {
const { api } = await import('/js/api.js');
const r = await api.get(`/activities/?contact_id=${this.id}&limit=200`);
this.subLists.activities = Array.isArray(r) ? r : (r.items || []);
} finally { this.loadingSub = false; }
},
async loadNotes() {
this.tab = 'notes';
this.loadingSub = true;
try {
const { api } = await import('/js/api.js');
const r = await api.get(`/notes/?parent_type=contact&parent_id=${this.id}`);
this.subLists.notes = Array.isArray(r) ? r : (r.items || []);
} catch { this.subLists.notes = []; } finally { this.loadingSub = false; }
},
tabBtn(t) {
return this.tab === t
? 'flex-1 py-2 px-4 text-sm font-medium border-b-2 border-blue-600 text-blue-600'
: 'flex-1 py-2 px-4 text-sm font-medium border-b-2 border-transparent text-slate-500 hover:text-slate-700';
},
openEdit() { this.showEdit = true; this.editError = ''; },
closeEdit() { this.showEdit = false; },
async submitEdit() {
this.saving = true; this.editError = '';
try {
const { api } = await import('/js/api.js');
const p = { first_name: this.editForm.first_name, last_name: this.editForm.last_name };
if (this.editForm.email) p.email = this.editForm.email;
if (this.editForm.phone) p.phone = this.editForm.phone;
if (this.editForm.account_id) p.account_id = Number(this.editForm.account_id);
await api.patch(`/contacts/${this.id}`, p);
Alpine.store('notifications').success('Contact aktualisiert.');
this.showEdit = false;
await this.load();
} catch (err) {
this.editError = err.message || 'Fehler.';
} finally { this.saving = false; }
},
async confirmDelete() {
if (!confirm('Contact wirklich löschen?')) return;
try {
const { api } = await import('/js/api.js');
await api.del(`/contacts/${this.id}`);
Alpine.store('notifications').success('Contact gelöscht.');
window.location.href = '/contacts.html';
} catch (err) {
Alpine.store('notifications').error(err.message || 'Fehler.');
}
},
formatDateTime(iso) {
if (!iso) return '';
try { return new Date(iso).toLocaleString('de-DE'); } catch { return iso; }
},
};
}
</script>
</body>
</html>