Files
leocrm/app/webui/accounts-detail.html
T

351 lines
18 KiB
HTML
Raw 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 Account 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" class="active">Accounts</a>
<a href="/contacts.html">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">Account-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="accountDetail()" x-init="init()">
<!-- Loading -->
<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 Account …</p>
</div>
<div x-show="!loading && account">
<!-- Header -->
<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="account?.name"></h2>
<p class="text-slate-500 text-sm mt-1">
<span class="crm-badge badge-blue" x-text="account?.industry || ''"></span>
<span class="crm-badge badge-gray ml-1" x-text="account?.size || ''"></span>
<span class="ml-2">Owner #<span x-text="account?.owner_id"></span></span>
</p>
<p class="text-blue-600 underline mt-2" x-show="account?.website">
<a :href="account?.website" target="_blank" x-text="account?.website"></a>
</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>
<!-- Tabs -->
<div class="flex border-b border-slate-200 mb-4" role="tablist">
<button @click="tab='info'" :class="tabBtn('info')">Info</button>
<button @click="loadContacts()" :class="tabBtn('contacts')">Contacts</button>
<button @click="loadDeals()" :class="tabBtn('deals')">Deals</button>
<button @click="loadActivities()" :class="tabBtn('activities')">Activities</button>
<button @click="loadNotes()" :class="tabBtn('notes')">Notes</button>
</div>
<!-- Info -->
<div x-show="tab === 'info'" class="crm-card">
<dl class="grid grid-cols-2 gap-4 text-sm">
<div><dt class="text-slate-500">Name</dt><dd class="font-medium" x-text="account?.name"></dd></div>
<div><dt class="text-slate-500">Branche</dt><dd x-text="account?.industry || ''"></dd></div>
<div><dt class="text-slate-500">Größe</dt><dd x-text="account?.size || ''"></dd></div>
<div><dt class="text-slate-500">Website</dt><dd x-text="account?.website || ''"></dd></div>
<div><dt class="text-slate-500">Org-ID</dt><dd x-text="account?.org_id"></dd></div>
<div><dt class="text-slate-500">Owner</dt><dd x-text="account?.owner_id"></dd></div>
<div><dt class="text-slate-500">Erstellt</dt><dd x-text="formatDateTime(account?.created_at)"></dd></div>
<div><dt class="text-slate-500">Aktualisiert</dt><dd x-text="formatDateTime(account?.updated_at)"></dd></div>
</dl>
</div>
<!-- Contacts -->
<div x-show="tab === 'contacts'" class="crm-card">
<div x-show="loadingSub" class="text-center py-4">
<div class="crm-spinner mx-auto"></div>
</div>
<table class="crm-table" x-show="!loadingSub">
<thead><tr><th>Name</th><th>Email</th><th>Phone</th></tr></thead>
<tbody>
<template x-for="c in subLists.contacts" :key="c.id">
<tr class="cursor-pointer"
@click="window.location.href = '/contacts-detail.html?id=' + c.id">
<td x-text="(c.first_name || '') + ' ' + (c.last_name || '')"></td>
<td x-text="c.email || ''"></td>
<td x-text="c.phone || ''"></td>
</tr>
</template>
<tr x-show="!loadingSub && subLists.contacts.length === 0">
<td colspan="3" class="text-center text-slate-500 py-3">Keine Contacts.</td>
</tr>
</tbody>
</table>
</div>
<!-- Deals -->
<div x-show="tab === 'deals'" class="crm-card">
<div x-show="loadingSub" class="text-center py-4"><div class="crm-spinner mx-auto"></div></div>
<table class="crm-table" x-show="!loadingSub">
<thead><tr><th>Title</th><th>Stage</th><th>Value</th><th>Close</th></tr></thead>
<tbody>
<template x-for="d in subLists.deals" :key="d.id">
<tr>
<td x-text="d.title"></td>
<td><span class="crm-badge badge-blue" x-text="d.stage"></span></td>
<td x-text="formatCurrency(d.value)"></td>
<td x-text="d.close_date || ''"></td>
</tr>
</template>
<tr x-show="!loadingSub && subLists.deals.length === 0">
<td colspan="4" class="text-center text-slate-500 py-3">Keine Deals.</td>
</tr>
</tbody>
</table>
</div>
<!-- Activities -->
<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>
<!-- Notes -->
<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">Account bearbeiten</h2>
<form @submit.prevent="submitEdit()">
<div class="mb-3">
<label class="crm-label">Name *</label>
<input type="text" required x-model="editForm.name" class="crm-input" />
</div>
<div class="mb-3">
<label class="crm-label">Website</label>
<input type="text" x-model="editForm.website" class="crm-input" />
</div>
<div class="grid grid-cols-2 gap-3 mb-3">
<div>
<label class="crm-label">Branche</label>
<input type="text" x-model="editForm.industry" class="crm-input" />
</div>
<div>
<label class="crm-label">Größe</label>
<input type="text" x-model="editForm.size" class="crm-input" />
</div>
</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 accountDetail() {
return {
id: null,
account: null,
loading: true,
tab: 'info',
subLists: { contacts: [], deals: [], activities: [], notes: [] },
loadingSub: false,
showEdit: false,
editForm: { name: '', website: '', industry: '', size: '' },
editError: '',
saving: false,
async init() {
const params = new URLSearchParams(window.location.search);
this.id = params.get('id');
if (!this.id) { window.location.href = '/accounts.html'; return; }
await this.load();
},
async load() {
this.loading = true;
try {
const { api } = await import('/js/api.js');
this.account = await api.get(`/accounts/${this.id}`);
this.editForm = {
name: this.account.name || '',
website: this.account.website || '',
industry: this.account.industry || '',
size: this.account.size || '',
};
} catch (err) {
Alpine.store('notifications').error(err.message || 'Fehler beim Laden.');
} finally { this.loading = false; }
},
async loadContacts() {
this.tab = 'contacts';
this.loadingSub = true;
try {
const { api } = await import('/js/api.js');
const r = await api.get(`/contacts/?account_id=${this.id}&limit=200`);
this.subLists.contacts = Array.isArray(r) ? r : (r.items || []);
} finally { this.loadingSub = false; }
},
async loadDeals() {
this.tab = 'deals';
this.loadingSub = true;
try {
const { api } = await import('/js/api.js');
const r = await api.get(`/deals/?account_id=${this.id}&limit=200`);
// Backend may not have account_id filter; client-filter as fallback
const list = Array.isArray(r) ? r : (r.items || []);
this.subLists.deals = list.filter(d => String(d.account_id) === String(this.id));
} finally { this.loadingSub = false; }
},
async loadActivities() {
this.tab = 'activities';
this.loadingSub = true;
try {
const { api } = await import('/js/api.js');
const r = await api.get(`/activities/?account_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=account&parent_id=${this.id}`);
this.subLists.notes = Array.isArray(r) ? r : (r.items || []);
} catch (err) {
// /notes may not be paginated; show empty on error
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 payload = { name: this.editForm.name };
if (this.editForm.website) payload.website = this.editForm.website;
if (this.editForm.industry) payload.industry = this.editForm.industry;
if (this.editForm.size) payload.size = this.editForm.size;
await api.patch(`/accounts/${this.id}`, payload);
Alpine.store('notifications').success('Account aktualisiert.');
this.showEdit = false;
await this.load();
} catch (err) {
this.editError = err.message || 'Fehler beim Speichern.';
} finally { this.saving = false; }
},
async confirmDelete() {
if (!confirm('Account wirklich löschen (Soft-Delete)?')) return;
try {
const { api } = await import('/js/api.js');
await api.del(`/accounts/${this.id}`);
Alpine.store('notifications').success('Account gelöscht.');
window.location.href = '/accounts.html';
} catch (err) {
Alpine.store('notifications').error(err.message || 'Löschen fehlgeschlagen.');
}
},
formatCurrency(v) {
return Number(v || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
},
formatDateTime(iso) {
if (!iso) return '';
try { return new Date(iso).toLocaleString('de-DE'); } catch { return iso; }
},
};
}
</script>
</body>
</html>