Files

232 lines
11 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 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>