Upload app/webui/contacts.html
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>CRM – Contacts</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">Contacts</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="contactList()" x-init="init()">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="crm-card mb-4 flex flex-wrap items-end gap-3">
|
||||
<div class="flex-1 min-w-[12rem]">
|
||||
<label class="crm-label">Suche (Name / E-Mail)</label>
|
||||
<input type="text" x-model="q" @keyup.enter="search()"
|
||||
class="crm-input" placeholder="z.B. Müller oder max@…" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="crm-label">Account-ID</label>
|
||||
<input type="number" min="1" x-model="accountId"
|
||||
class="crm-input" placeholder="optional" />
|
||||
</div>
|
||||
<button @click="search()" class="btn-primary">Suchen</button>
|
||||
<button @click="openCreate()" class="btn-primary ml-auto">+ New Contact</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div x-show="loading" class="crm-card text-center py-8">
|
||||
<div class="crm-spinner mx-auto"></div>
|
||||
<p class="text-slate-500 mt-2 text-sm">Lade Contacts …</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div x-show="!loading" class="overflow-x-auto">
|
||||
<table class="crm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Telefon</th>
|
||||
<th>Account-ID</th>
|
||||
<th>Owner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="c in contacts" :key="c.id">
|
||||
<tr class="cursor-pointer"
|
||||
@click="window.location.href = '/contacts-detail.html?id=' + c.id">
|
||||
<td class="font-medium"
|
||||
x-text="(c.first_name || '') + ' ' + (c.last_name || '')"></td>
|
||||
<td x-text="c.email || '–'"></td>
|
||||
<td x-text="c.phone || '–'"></td>
|
||||
<td x-text="c.account_id || '–'"></td>
|
||||
<td x-text="c.owner_id"></td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr x-show="!loading && contacts.length === 0">
|
||||
<td colspan="5" class="text-center text-slate-500 py-6">
|
||||
Keine Contacts gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-between mt-4 text-sm text-slate-600"
|
||||
x-show="!loading">
|
||||
<span x-text="pageInfo"></span>
|
||||
<div class="flex gap-2">
|
||||
<button @click="prevPage()" :disabled="skip === 0" class="btn-secondary">← Zurück</button>
|
||||
<button @click="nextPage()" :disabled="skip + limit >= total" class="btn-secondary">Weiter →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create modal -->
|
||||
<div x-show="showCreate" x-cloak class="crm-modal-backdrop"
|
||||
@click.self="closeCreate()">
|
||||
<div class="crm-modal p-6">
|
||||
<h2 class="text-lg font-semibold mb-4">Neuer Contact</h2>
|
||||
<form @submit.prevent="submitCreate()">
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div>
|
||||
<label class="crm-label">Vorname *</label>
|
||||
<input type="text" required maxlength="128"
|
||||
x-model="createForm.first_name" class="crm-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="crm-label">Nachname *</label>
|
||||
<input type="text" required maxlength="128"
|
||||
x-model="createForm.last_name" class="crm-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="crm-label">E-Mail</label>
|
||||
<input type="email" x-model="createForm.email" class="crm-input" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="crm-label">Telefon</label>
|
||||
<input type="text" maxlength="64"
|
||||
x-model="createForm.phone" class="crm-input" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="crm-label">Account-ID (optional)</label>
|
||||
<input type="number" min="1"
|
||||
x-model="createForm.account_id" class="crm-input" />
|
||||
</div>
|
||||
<div x-show="createError" class="mb-3 p-2 bg-red-50 text-red-700 text-sm rounded"
|
||||
x-text="createError"></div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" @click="closeCreate()" class="btn-secondary">Abbrechen</button>
|
||||
<button type="submit" :disabled="creating" class="btn-primary"
|
||||
x-text="creating ? 'Speichere …' : 'Anlegen'"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function contactList() {
|
||||
return {
|
||||
contacts: [],
|
||||
total: 0,
|
||||
skip: 0,
|
||||
limit: 20,
|
||||
q: '',
|
||||
accountId: '',
|
||||
loading: false,
|
||||
error: '',
|
||||
showCreate: false,
|
||||
createForm: { first_name: '', last_name: '', email: '', phone: '', account_id: '' },
|
||||
createError: '',
|
||||
creating: false,
|
||||
|
||||
async init() { await this.load(); },
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const { api } = await import('/js/api.js');
|
||||
const params = new URLSearchParams();
|
||||
params.set('skip', String(this.skip));
|
||||
params.set('limit', String(this.limit));
|
||||
if (this.q) params.set('q', this.q);
|
||||
if (this.accountId) params.set('account_id', String(this.accountId));
|
||||
const r = await api.get(`/contacts/?${params}`);
|
||||
this.contacts = Array.isArray(r) ? r : (r.items || []);
|
||||
this.total = (r && r.total) || this.contacts.length;
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
Alpine.store('notifications').error(err.message || 'Lade-Fehler.');
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
async search() { this.skip = 0; await this.load(); },
|
||||
async nextPage() { if (this.skip + this.limit < this.total) { this.skip += this.limit; await this.load(); } },
|
||||
async prevPage() { if (this.skip - this.limit >= 0) { this.skip -= this.limit; await this.load(); } },
|
||||
get pageInfo() {
|
||||
const from = this.total === 0 ? 0 : this.skip + 1;
|
||||
const to = Math.min(this.skip + this.limit, this.total);
|
||||
return `${from}–${to} von ${this.total}`;
|
||||
},
|
||||
openCreate() { this.createForm = { first_name: '', last_name: '', email: '', phone: '', account_id: '' }; this.createError = ''; this.showCreate = true; },
|
||||
closeCreate() { this.showCreate = false; },
|
||||
async submitCreate() {
|
||||
this.creating = true; this.createError = '';
|
||||
try {
|
||||
const { api } = await import('/js/api.js');
|
||||
const p = { first_name: this.createForm.first_name, last_name: this.createForm.last_name };
|
||||
if (this.createForm.email) p.email = this.createForm.email;
|
||||
if (this.createForm.phone) p.phone = this.createForm.phone;
|
||||
if (this.createForm.account_id) p.account_id = Number(this.createForm.account_id);
|
||||
await api.post('/contacts/', p);
|
||||
Alpine.store('notifications').success('Contact angelegt.');
|
||||
this.showCreate = false;
|
||||
await this.load();
|
||||
} catch (err) {
|
||||
this.createError = err.message || 'Fehler beim Anlegen.';
|
||||
} finally { this.creating = false; }
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user