168 lines
8.3 KiB
HTML
168 lines
8.3 KiB
HTML
|
|
<!doctype html>
|
|||
|
|
<html lang="de">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|||
|
|
<title>CRM – Pipeline</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>
|
|||
|
|
<script type="module" src="/components/deal-kanban.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">Contacts</a>
|
|||
|
|
<a href="/pipeline.html" class="active">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">Pipeline</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="dealKanban()" x-init="init()">
|
|||
|
|
|
|||
|
|
<!-- Toolbar -->
|
|||
|
|
<div class="flex items-center justify-between mb-4">
|
|||
|
|
<div class="text-sm text-slate-600"
|
|||
|
|
x-show="!loading">
|
|||
|
|
Ziehe Karten zwischen Spalten, um die Stage zu ändern.
|
|||
|
|
</div>
|
|||
|
|
<div x-show="loading" class="crm-spinner"></div>
|
|||
|
|
<div class="flex gap-2 items-center">
|
|||
|
|
<input type="number" min="1" x-model="ownerId" placeholder="Owner-ID (optional)"
|
|||
|
|
class="crm-input" style="width: 12rem;" />
|
|||
|
|
<button @click="filterByOwner()" class="btn-secondary">Filtern</button>
|
|||
|
|
<button @click="openCreate()" class="btn-primary">+ New Deal</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div x-show="error" class="mb-4 p-3 bg-red-50 text-red-700 rounded text-sm"
|
|||
|
|
x-text="error"></div>
|
|||
|
|
|
|||
|
|
<!-- Kanban board -->
|
|||
|
|
<div class="crm-kanban">
|
|||
|
|
<template x-for="stage in stages" :key="stage">
|
|||
|
|
<div class="crm-kanban-col"
|
|||
|
|
:class="{ 'drag-over': dragOverStage === stage }"
|
|||
|
|
@dragover.prevent="onDragOver($event, stage)"
|
|||
|
|
@dragleave="onDragLeave(stage)"
|
|||
|
|
@drop="onDrop($event, stage)">
|
|||
|
|
<div class="flex items-center justify-between mb-2 px-1">
|
|||
|
|
<h3 class="font-semibold text-slate-700 text-sm" x-text="stageLabel(stage)"></h3>
|
|||
|
|
<span class="crm-badge badge-gray text-xs" x-text="columnCount(stage)"></span>
|
|||
|
|
</div>
|
|||
|
|
<p class="text-xs text-slate-500 px-1 mb-2" x-text="columnValue(stage)"></p>
|
|||
|
|
|
|||
|
|
<template x-for="deal in (dealsByStage[stage] || [])" :key="deal.id">
|
|||
|
|
<div class="crm-kanban-card"
|
|||
|
|
draggable="true"
|
|||
|
|
@dragstart="onDragStart($event, deal.id)">
|
|||
|
|
<p class="font-medium text-slate-900" x-text="deal.title"></p>
|
|||
|
|
<p class="text-xs text-slate-500 mt-1">
|
|||
|
|
<span x-text="formatCurrency(deal.value)"></span>
|
|||
|
|
</p>
|
|||
|
|
<p class="text-xs text-slate-400 mt-1" x-show="deal.close_date">
|
|||
|
|
Close: <span x-text="formatDate(deal.close_date)"></span>
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<div x-show="columnCount(stage) === 0"
|
|||
|
|
class="text-center text-xs text-slate-400 py-4">
|
|||
|
|
Keine Deals
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</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 Deal</h2>
|
|||
|
|
<form @submit.prevent="submitCreate()">
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label class="crm-label">Titel *</label>
|
|||
|
|
<input type="text" required maxlength="255"
|
|||
|
|
x-model="createForm.title" class="crm-input" />
|
|||
|
|
</div>
|
|||
|
|
<div class="grid grid-cols-2 gap-3 mb-3">
|
|||
|
|
<div>
|
|||
|
|
<label class="crm-label">Wert (€)</label>
|
|||
|
|
<input type="number" min="0" step="0.01"
|
|||
|
|
x-model.number="createForm.value" class="crm-input" />
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label class="crm-label">Währung</label>
|
|||
|
|
<input type="text" maxlength="3" minlength="3"
|
|||
|
|
x-model="createForm.currency" class="crm-input" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="grid grid-cols-2 gap-3 mb-3">
|
|||
|
|
<div>
|
|||
|
|
<label class="crm-label">Stage</label>
|
|||
|
|
<select x-model="createForm.stage" class="crm-input">
|
|||
|
|
<template x-for="s in stages" :key="s">
|
|||
|
|
<option :value="s" x-text="stageLabel(s)"></option>
|
|||
|
|
</template>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label class="crm-label">Account-ID *</label>
|
|||
|
|
<input type="number" min="1" required
|
|||
|
|
x-model="createForm.account_id" class="crm-input" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label class="crm-label">Close-Datum</label>
|
|||
|
|
<input type="date" x-model="createForm.close_date" 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>
|
|||
|
|
|
|||
|
|
</body>
|
|||
|
|
</html>
|