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

269 lines
14 KiB
HTML
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.
<!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>