aktueller stand
This commit is contained in:
105
src/web/templates/admin/base.html
Normal file
105
src/web/templates/admin/base.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Admin - LinkedIn Posts{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'brand': {
|
||||
'bg': '#3d4848',
|
||||
'bg-light': '#4a5858',
|
||||
'bg-dark': '#2d3838',
|
||||
'highlight': '#ffc700',
|
||||
'highlight-dark': '#e6b300',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { background-color: #3d4848; }
|
||||
.nav-link.active { background-color: #ffc700; color: #2d3838; }
|
||||
.nav-link.active svg { stroke: #2d3838; }
|
||||
.post-content { white-space: pre-wrap; word-wrap: break-word; }
|
||||
.btn-primary { background-color: #ffc700; color: #2d3838; }
|
||||
.btn-primary:hover { background-color: #e6b300; }
|
||||
.sidebar-bg { background-color: #2d3838; }
|
||||
.card-bg { background-color: #4a5858; border-color: #5a6868; }
|
||||
.input-bg { background-color: #3d4848; border-color: #5a6868; }
|
||||
.input-bg:focus { border-color: #ffc700; outline: none; }
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: #3d4848; }
|
||||
::-webkit-scrollbar-thumb { background: #5a6868; border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #6a7878; }
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="text-gray-100 min-h-screen flex">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full">
|
||||
<div class="p-4 border-b border-gray-600">
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<div>
|
||||
<img src="/static/logo.png" alt="Logo" class="h-15 w-auto">
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<span class="text-xs text-brand-highlight font-medium px-2 py-1 bg-brand-highlight/20 rounded">ADMIN</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 p-4 space-y-2">
|
||||
<a href="/admin" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'home' %}active{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/admin/customers/new" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'new_customer' %}active{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/></svg>
|
||||
Neuer Kunde
|
||||
</a>
|
||||
<a href="/admin/research" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'research' %}active{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
Research Topics
|
||||
</a>
|
||||
<a href="/admin/create" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'create' %}active{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
Post erstellen
|
||||
</a>
|
||||
<a href="/admin/posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'posts' %}active{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
Alle Posts
|
||||
</a>
|
||||
<a href="/admin/scraped-posts" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'scraped_posts' %}active{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>
|
||||
Post-Typen
|
||||
</a>
|
||||
<a href="/admin/status" class="nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:bg-brand-bg-light transition-colors {% if page == 'status' %}active{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
|
||||
Status
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="p-4 border-t border-gray-600">
|
||||
<a href="/admin/logout" class="flex items-center gap-2 text-gray-400 hover:text-gray-200 text-sm transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 ml-64">
|
||||
<div class="p-8">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
539
src/web/templates/admin/create_post.html
Normal file
539
src/web/templates/admin/create_post.html
Normal file
@@ -0,0 +1,539 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Post erstellen - LinkedIn Posts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Post erstellen</h1>
|
||||
<p class="text-gray-400">Generiere einen neuen LinkedIn Post mit AI</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left: Form -->
|
||||
<div>
|
||||
<form id="createPostForm" class="card-bg rounded-xl border p-6 space-y-6">
|
||||
<!-- Customer Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
|
||||
<select name="customer_id" id="customerSelect" required class="w-full input-bg border rounded-lg px-4 py-3 text-white">
|
||||
<option value="">-- Kunde wählen --</option>
|
||||
{% for customer in customers %}
|
||||
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Post Type Selection -->
|
||||
<div id="postTypeSelectionArea" class="hidden">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ auswählen (optional)</label>
|
||||
<div id="postTypeCards" class="flex flex-wrap gap-2 mb-2">
|
||||
<!-- Post type cards will be loaded here -->
|
||||
</div>
|
||||
<input type="hidden" id="selectedPostTypeId" value="">
|
||||
</div>
|
||||
|
||||
<!-- Topic Selection -->
|
||||
<div id="topicSelectionArea" class="hidden">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Topic auswählen</label>
|
||||
<div id="topicsList" class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<p class="text-gray-500">Lade Topics...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Topic -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||||
<span>Oder eigenes Topic eingeben</span>
|
||||
</label>
|
||||
<div class="space-y-3">
|
||||
<input type="text" id="customTopicTitle" placeholder="Topic Titel" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
|
||||
<textarea id="customTopicFact" rows="3" placeholder="Fakt / Kernaussage zum Topic..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
|
||||
<input type="text" id="customTopicSource" placeholder="Quelle (optional)" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Area -->
|
||||
<div id="progressArea" class="hidden">
|
||||
<div class="bg-brand-bg rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span id="progressMessage" class="text-gray-300">Starte Post-Erstellung...</span>
|
||||
<span id="progressPercent" class="text-gray-400">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-brand-bg-dark rounded-full h-2">
|
||||
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<div id="iterationInfo" class="mt-2 text-sm text-gray-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
Post generieren
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if not customers %}
|
||||
<div class="mt-4 bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
|
||||
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/admin/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right: Result -->
|
||||
<div>
|
||||
<div id="resultArea" class="card-bg rounded-xl border p-6">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Generierter Post</h3>
|
||||
|
||||
<!-- Live Versions Display -->
|
||||
<div id="liveVersions" class="hidden space-y-4 mb-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm text-gray-400">Live-Vorschau der Iterationen:</span>
|
||||
</div>
|
||||
<div id="versionsContainer" class="space-y-4"></div>
|
||||
</div>
|
||||
|
||||
<div id="postResult">
|
||||
<p class="text-gray-400">Wähle einen Kunden und ein Topic, dann klicke auf "Post generieren"...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const form = document.getElementById('createPostForm');
|
||||
const customerSelect = document.getElementById('customerSelect');
|
||||
const topicSelectionArea = document.getElementById('topicSelectionArea');
|
||||
const topicsList = document.getElementById('topicsList');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const progressArea = document.getElementById('progressArea');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressMessage = document.getElementById('progressMessage');
|
||||
const progressPercent = document.getElementById('progressPercent');
|
||||
const iterationInfo = document.getElementById('iterationInfo');
|
||||
const postResult = document.getElementById('postResult');
|
||||
const liveVersions = document.getElementById('liveVersions');
|
||||
const versionsContainer = document.getElementById('versionsContainer');
|
||||
const postTypeSelectionArea = document.getElementById('postTypeSelectionArea');
|
||||
const postTypeCards = document.getElementById('postTypeCards');
|
||||
const selectedPostTypeIdInput = document.getElementById('selectedPostTypeId');
|
||||
|
||||
let selectedTopic = null;
|
||||
let currentVersionIndex = 0;
|
||||
let currentPostTypes = [];
|
||||
let currentTopics = [];
|
||||
|
||||
function renderVersions(versions, feedbackList) {
|
||||
if (!versions || versions.length === 0) {
|
||||
liveVersions.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
liveVersions.classList.remove('hidden');
|
||||
|
||||
// Build version tabs and content
|
||||
let html = `
|
||||
<div class="flex gap-2 mb-4 flex-wrap">
|
||||
${versions.map((_, i) => `
|
||||
<button onclick="showVersion(${i})" id="versionTab${i}"
|
||||
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
|
||||
${i === currentVersionIndex ? 'bg-brand-highlight text-brand-bg-dark' : 'bg-brand-bg text-gray-300 hover:bg-brand-bg-light'}">
|
||||
V${i + 1}
|
||||
${feedbackList[i] ? `<span class="ml-1 text-xs opacity-75">(${feedbackList[i].overall_score || '?'})</span>` : ''}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show current version
|
||||
const currentVersion = versions[currentVersionIndex];
|
||||
const currentFeedback = feedbackList[currentVersionIndex];
|
||||
|
||||
html += `
|
||||
<div class="grid grid-cols-1 ${currentFeedback ? 'lg:grid-cols-2' : ''} gap-4">
|
||||
<div class="bg-brand-bg/50 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium text-gray-300">Version ${currentVersionIndex + 1}</span>
|
||||
${currentFeedback ? `
|
||||
<span class="px-2 py-0.5 text-xs rounded ${currentFeedback.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
|
||||
${currentFeedback.approved ? 'Approved' : `Score: ${currentFeedback.overall_score}/100`}
|
||||
</span>
|
||||
` : '<span class="text-xs text-gray-500">Wird bewertet...</span>'}
|
||||
</div>
|
||||
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm max-h-96 overflow-y-auto">${currentVersion}</pre>
|
||||
</div>
|
||||
${currentFeedback ? `
|
||||
<div class="bg-brand-bg/30 rounded-lg p-4 border border-brand-bg-light">
|
||||
<span class="text-sm font-medium text-gray-300 block mb-2">Kritik</span>
|
||||
<p class="text-sm text-gray-400 mb-3">${currentFeedback.feedback || 'Keine Kritik'}</p>
|
||||
${currentFeedback.improvements && currentFeedback.improvements.length > 0 ? `
|
||||
<div class="mt-2">
|
||||
<span class="text-xs font-medium text-gray-400">Verbesserungen:</span>
|
||||
<ul class="mt-1 space-y-1">
|
||||
${currentFeedback.improvements.map(imp => `
|
||||
<li class="text-xs text-gray-500 flex items-start gap-1">
|
||||
<span class="text-yellow-500">•</span> ${imp}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${currentFeedback.scores ? `
|
||||
<div class="mt-3 pt-3 border-t border-brand-bg-light">
|
||||
<div class="grid grid-cols-3 gap-2 text-xs">
|
||||
<div class="text-center">
|
||||
<div class="text-gray-500">Authentizität</div>
|
||||
<div class="font-medium text-gray-300">${currentFeedback.scores.authenticity_and_style || '?'}/40</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-gray-500">Content</div>
|
||||
<div class="font-medium text-gray-300">${currentFeedback.scores.content_quality || '?'}/35</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-gray-500">Technik</div>
|
||||
<div class="font-medium text-gray-300">${currentFeedback.scores.technical_execution || '?'}/25</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
versionsContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function showVersion(index) {
|
||||
currentVersionIndex = index;
|
||||
// Get cached versions from progress store
|
||||
const cachedData = window.lastProgressData;
|
||||
if (cachedData) {
|
||||
renderVersions(cachedData.versions, cachedData.feedback_list);
|
||||
}
|
||||
}
|
||||
|
||||
// Load topics and post types when customer is selected
|
||||
customerSelect.addEventListener('change', async () => {
|
||||
const customerId = customerSelect.value;
|
||||
selectedPostTypeIdInput.value = '';
|
||||
|
||||
if (!customerId) {
|
||||
topicSelectionArea.classList.add('hidden');
|
||||
postTypeSelectionArea.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
topicSelectionArea.classList.remove('hidden');
|
||||
topicsList.innerHTML = '<p class="text-gray-500">Lade Topics...</p>';
|
||||
|
||||
// Load post types
|
||||
try {
|
||||
const ptResponse = await fetch(`/admin/api/customers/${customerId}/post-types`);
|
||||
const ptData = await ptResponse.json();
|
||||
|
||||
if (ptData.post_types && ptData.post_types.length > 0) {
|
||||
currentPostTypes = ptData.post_types;
|
||||
postTypeSelectionArea.classList.remove('hidden');
|
||||
|
||||
postTypeCards.innerHTML = `
|
||||
<button type="button" onclick="selectPostTypeForCreate('')" id="ptc_all"
|
||||
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
|
||||
Alle Typen
|
||||
</button>
|
||||
` + ptData.post_types.map(pt => `
|
||||
<button type="button" onclick="selectPostTypeForCreate('${pt.id}')" id="ptc_${pt.id}"
|
||||
class="px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
|
||||
${pt.name}
|
||||
${pt.has_analysis ? '<span class="ml-1 text-green-400 text-xs">*</span>' : ''}
|
||||
</button>
|
||||
`).join('');
|
||||
} else {
|
||||
postTypeSelectionArea.classList.add('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load post types:', error);
|
||||
postTypeSelectionArea.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Load topics
|
||||
try {
|
||||
const response = await fetch(`/admin/api/customers/${customerId}/topics`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.topics && data.topics.length > 0) {
|
||||
renderTopicsList(data);
|
||||
} else {
|
||||
// No topics available - show helpful message
|
||||
let message = '';
|
||||
if (data.used_count > 0) {
|
||||
message = `<div class="text-center py-4">
|
||||
<p class="text-gray-400 mb-2">Alle ${data.used_count} Topics wurden bereits verwendet.</p>
|
||||
<a href="/admin/research" class="text-brand-highlight hover:underline">Neue Topics recherchieren</a>
|
||||
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
|
||||
</div>`;
|
||||
} else {
|
||||
message = `<div class="text-center py-4">
|
||||
<p class="text-gray-400 mb-2">Keine Topics gefunden.</p>
|
||||
<a href="/admin/research" class="text-brand-highlight hover:underline">Recherche starten</a>
|
||||
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
|
||||
</div>`;
|
||||
}
|
||||
topicsList.innerHTML = message;
|
||||
}
|
||||
} catch (error) {
|
||||
topicsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear selected topic when custom topic is entered
|
||||
['customTopicTitle', 'customTopicFact', 'customTopicSource'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('input', () => {
|
||||
selectedTopic = null;
|
||||
document.querySelectorAll('input[name="topic"]').forEach(radio => radio.checked = false);
|
||||
});
|
||||
});
|
||||
|
||||
function selectPostTypeForCreate(typeId) {
|
||||
selectedPostTypeIdInput.value = typeId;
|
||||
|
||||
// Update card styles
|
||||
document.querySelectorAll('[id^="ptc_"]').forEach(card => {
|
||||
if (card.id === `ptc_${typeId}` || (typeId === '' && card.id === 'ptc_all')) {
|
||||
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
|
||||
} else {
|
||||
card.className = 'px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
|
||||
}
|
||||
});
|
||||
|
||||
// Optionally reload topics filtered by post type
|
||||
const customerId = customerSelect.value;
|
||||
if (customerId) {
|
||||
loadTopicsForPostType(customerId, typeId);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTopicsForPostType(customerId, postTypeId) {
|
||||
topicsList.innerHTML = '<p class="text-gray-500">Lade Topics...</p>';
|
||||
|
||||
try {
|
||||
let url = `/api/customers/${customerId}/topics`;
|
||||
if (postTypeId) {
|
||||
url += `?post_type_id=${postTypeId}`;
|
||||
}
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.topics && data.topics.length > 0) {
|
||||
renderTopicsList(data);
|
||||
} else {
|
||||
let message = '';
|
||||
if (data.used_count > 0) {
|
||||
message = `<div class="text-center py-4">
|
||||
<p class="text-gray-400 mb-2">Alle ${data.used_count} Topics wurden bereits verwendet.</p>
|
||||
<a href="/admin/research" class="text-brand-highlight hover:underline">Neue Topics recherchieren</a>
|
||||
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
|
||||
</div>`;
|
||||
} else {
|
||||
message = `<div class="text-center py-4">
|
||||
<p class="text-gray-400 mb-2">Keine Topics gefunden${postTypeId ? ' für diesen Post-Typ' : ''}.</p>
|
||||
<a href="/admin/research" class="text-brand-highlight hover:underline">Recherche starten</a>
|
||||
<p class="text-gray-500 text-sm mt-2">oder gib unten ein eigenes Topic ein.</p>
|
||||
</div>`;
|
||||
}
|
||||
topicsList.innerHTML = message;
|
||||
}
|
||||
} catch (error) {
|
||||
topicsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTopicsList(data) {
|
||||
// Store topics in global array for safe access
|
||||
currentTopics = data.topics;
|
||||
|
||||
// Reset selected topic when list is re-rendered
|
||||
selectedTopic = null;
|
||||
|
||||
let statsHtml = '';
|
||||
if (data.used_count > 0) {
|
||||
statsHtml = `<p class="text-xs text-gray-500 mb-3">${data.available_count} verfügbar · ${data.used_count} bereits verwendet</p>`;
|
||||
}
|
||||
|
||||
topicsList.innerHTML = statsHtml + data.topics.map((topic, i) => `
|
||||
<label class="flex items-start gap-3 p-3 bg-brand-bg/50 rounded-lg cursor-pointer hover:bg-brand-bg transition-colors border border-transparent hover:border-brand-highlight/30">
|
||||
<input type="radio" name="topic" value="${i}" class="mt-1 text-brand-highlight" data-topic-index="${i}">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded">${escapeHtml(topic.category || 'Topic')}</span>
|
||||
${topic.target_post_type_id ? `<span class="text-xs text-gray-500">Typ-spezifisch</span>` : ''}
|
||||
${topic.source ? `<span class="text-xs text-gray-500">🔗 ${escapeHtml(topic.source.substring(0, 30))}${topic.source.length > 30 ? '...' : ''}</span>` : ''}
|
||||
</div>
|
||||
<p class="font-medium text-white">${escapeHtml(topic.title)}</p>
|
||||
${topic.angle ? `<p class="text-xs text-brand-highlight/80 mt-1">→ ${escapeHtml(topic.angle)}</p>` : ''}
|
||||
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${escapeHtml(topic.hook_idea.substring(0, 120))}${topic.hook_idea.length > 120 ? '...' : ''}"</p>` : ''}
|
||||
${topic.key_facts && topic.key_facts.length > 0 ? `
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
${topic.key_facts.slice(0, 2).map(f => `<span class="text-xs bg-brand-bg-dark px-2 py-0.5 rounded text-gray-400">📊 ${escapeHtml(f.substring(0, 40))}${f.length > 40 ? '...' : ''}</span>`).join('')}
|
||||
</div>
|
||||
` : (topic.fact ? `<p class="text-sm text-gray-400 mt-1">${escapeHtml(topic.fact.substring(0, 100))}...</p>` : '')}
|
||||
${topic.why_this_person ? `<p class="text-xs text-gray-500 mt-2">💡 ${escapeHtml(topic.why_this_person.substring(0, 80))}${topic.why_this_person.length > 80 ? '...' : ''}</p>` : ''}
|
||||
</div>
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners to radio buttons
|
||||
document.querySelectorAll('input[name="topic"]').forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
const index = parseInt(radio.dataset.topicIndex, 10);
|
||||
selectedTopic = currentTopics[index];
|
||||
// Clear custom topic fields
|
||||
document.getElementById('customTopicTitle').value = '';
|
||||
document.getElementById('customTopicFact').value = '';
|
||||
document.getElementById('customTopicSource').value = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to escape HTML special characters
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const customerId = customerSelect.value;
|
||||
if (!customerId) {
|
||||
alert('Bitte wähle einen Kunden aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get topic (either selected or custom)
|
||||
let topic;
|
||||
const customTitle = document.getElementById('customTopicTitle').value.trim();
|
||||
const customFact = document.getElementById('customTopicFact').value.trim();
|
||||
|
||||
if (customTitle && customFact) {
|
||||
topic = {
|
||||
title: customTitle,
|
||||
fact: customFact,
|
||||
source: document.getElementById('customTopicSource').value.trim() || null,
|
||||
category: 'Custom'
|
||||
};
|
||||
} else if (selectedTopic) {
|
||||
topic = selectedTopic;
|
||||
} else {
|
||||
alert('Bitte wähle ein Topic aus oder gib ein eigenes ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Generiert...';
|
||||
progressArea.classList.remove('hidden');
|
||||
postResult.innerHTML = '<p class="text-gray-400">Post wird generiert...</p>';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('customer_id', customerId);
|
||||
formData.append('topic_json', JSON.stringify(topic));
|
||||
if (selectedPostTypeIdInput.value) {
|
||||
formData.append('post_type_id', selectedPostTypeIdInput.value);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/posts', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
const taskId = data.task_id;
|
||||
currentVersionIndex = 0;
|
||||
window.lastProgressData = null;
|
||||
|
||||
const pollInterval = setInterval(async () => {
|
||||
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
|
||||
const status = await statusResponse.json();
|
||||
|
||||
progressBar.style.width = `${status.progress}%`;
|
||||
progressPercent.textContent = `${status.progress}%`;
|
||||
progressMessage.textContent = status.message;
|
||||
|
||||
if (status.iteration !== undefined) {
|
||||
iterationInfo.textContent = `Iteration ${status.iteration}/${status.max_iterations}`;
|
||||
}
|
||||
|
||||
// Update live versions display
|
||||
if (status.versions && status.versions.length > 0) {
|
||||
window.lastProgressData = status;
|
||||
// Auto-select latest version
|
||||
if (status.versions.length > currentVersionIndex + 1) {
|
||||
currentVersionIndex = status.versions.length - 1;
|
||||
}
|
||||
renderVersions(status.versions, status.feedback_list || []);
|
||||
postResult.innerHTML = '<p class="text-gray-400">Siehe Live-Vorschau oben...</p>';
|
||||
}
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
progressArea.classList.add('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
|
||||
|
||||
// Keep live versions visible but update header
|
||||
const result = status.result;
|
||||
|
||||
postResult.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-2 text-sm flex-wrap">
|
||||
<span class="px-2 py-1 rounded ${result.approved ? 'bg-green-600/30 text-green-300' : 'bg-yellow-600/30 text-yellow-300'}">
|
||||
${result.approved ? 'Approved' : 'Review needed'}
|
||||
</span>
|
||||
<span class="text-gray-400">Score: ${result.final_score}/100</span>
|
||||
<span class="text-gray-400">Iterations: ${result.iterations}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 mb-2">Finaler Post:</div>
|
||||
<div class="bg-brand-bg/50 rounded-lg p-4">
|
||||
<pre class="whitespace-pre-wrap text-gray-200 font-sans">${result.final_post}</pre>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="copyPost()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
|
||||
In Zwischenablage kopieren
|
||||
</button>
|
||||
<button onclick="toggleVersions()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-white transition-colors">
|
||||
Versionen ${liveVersions.classList.contains('hidden') ? 'anzeigen' : 'ausblenden'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (status.status === 'error') {
|
||||
clearInterval(pollInterval);
|
||||
progressArea.classList.add('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
|
||||
postResult.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
progressArea.classList.add('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> Post generieren';
|
||||
postResult.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
function copyPost() {
|
||||
const postText = document.querySelector('#postResult pre').textContent;
|
||||
navigator.clipboard.writeText(postText).then(() => {
|
||||
alert('Post in Zwischenablage kopiert!');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleVersions() {
|
||||
liveVersions.classList.toggle('hidden');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
97
src/web/templates/admin/dashboard.html
Normal file
97
src/web/templates/admin/dashboard.html
Normal file
@@ -0,0 +1,97 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - LinkedIn Posts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Dashboard</h1>
|
||||
<p class="text-gray-400">Willkommen zum LinkedIn Post Creation System</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
|
||||
<strong>Error:</strong> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="card-bg rounded-xl border p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">Kunden</p>
|
||||
<p class="text-2xl font-bold text-white">{{ customers_count or 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-bg rounded-xl border p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-green-600/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">Generierte Posts</p>
|
||||
<p class="text-2xl font-bold text-white">{{ total_posts or 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-bg rounded-xl border p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">AI Agents</p>
|
||||
<p class="text-2xl font-bold text-white">5</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card-bg rounded-xl border p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Schnellaktionen</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<a href="/admin/customers/new" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
|
||||
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-white">Neuer Kunde</p>
|
||||
<p class="text-sm text-gray-400">Setup starten</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/admin/research" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
|
||||
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-white">Research</p>
|
||||
<p class="text-sm text-gray-400">Topics finden</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/admin/create" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
|
||||
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-white">Post erstellen</p>
|
||||
<p class="text-sm text-gray-400">Content generieren</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/admin/posts" class="flex items-center gap-3 p-4 bg-brand-bg rounded-lg hover:bg-brand-bg-light transition-colors">
|
||||
<div class="w-10 h-10 btn-primary rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-white">Alle Posts</p>
|
||||
<p class="text-sm text-gray-400">Übersicht anzeigen</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
72
src/web/templates/admin/login.html
Normal file
72
src/web/templates/admin/login.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - LinkedIn Posts</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'brand': {
|
||||
'bg': '#3d4848',
|
||||
'bg-light': '#4a5858',
|
||||
'bg-dark': '#2d3838',
|
||||
'highlight': '#ffc700',
|
||||
'highlight-dark': '#e6b300',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { background-color: #3d4848; }
|
||||
.btn-primary { background-color: #ffc700; color: #2d3838; }
|
||||
.btn-primary:hover { background-color: #e6b300; }
|
||||
.card-bg { background-color: #4a5858; border-color: #5a6868; }
|
||||
.input-bg { background-color: #3d4848; border-color: #5a6868; }
|
||||
.input-bg:focus { border-color: #ffc700; outline: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-gray-100 min-h-screen flex items-center justify-center">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="card-bg rounded-xl border p-8">
|
||||
<div class="text-center mb-8">
|
||||
<img src="/static/logo.png" alt="Logo" class="h-16 w-auto mx-auto mb-4">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">LinkedIn Posts</h1>
|
||||
<p class="text-gray-400">Admin Panel</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
|
||||
Falsches Passwort. Bitte versuche es erneut.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="/admin/login" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
autofocus
|
||||
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
|
||||
placeholder="Passwort eingeben..."
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full btn-primary font-medium py-3 rounded-lg transition-colors"
|
||||
>
|
||||
Anmelden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
274
src/web/templates/admin/new_customer.html
Normal file
274
src/web/templates/admin/new_customer.html
Normal file
@@ -0,0 +1,274 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Neuer Kunde - LinkedIn Posts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Neuer Kunde</h1>
|
||||
<p class="text-gray-400">Richte einen neuen Kunden ein und starte das initiale Setup</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl">
|
||||
<form id="customerForm" class="card-bg rounded-xl border p-6 space-y-6">
|
||||
<!-- Basic Info -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Basis-Informationen</h3>
|
||||
<div class="grid gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Name *</label>
|
||||
<input type="text" name="name" required class="w-full input-bg border rounded-lg px-4 py-2 text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">LinkedIn URL *</label>
|
||||
<input type="url" name="linkedin_url" required placeholder="https://www.linkedin.com/in/username" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Firma</label>
|
||||
<input type="text" name="company_name" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">E-Mail</label>
|
||||
<input type="email" name="email" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Persona -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Persona & Stil</h3>
|
||||
<div class="grid gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Persona</label>
|
||||
<textarea name="persona" rows="3" placeholder="Beschreibe die Expertise, Positionierung und den Charakter der Person..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Ansprache</label>
|
||||
<input type="text" name="form_of_address" placeholder="z.B. Duzen (Du/Euch) oder Siezen (Sie)" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Style Guide</label>
|
||||
<textarea name="style_guide" rows="3" placeholder="Beschreibe den Schreibstil, Tonalität und Richtlinien..." class="w-full input-bg border rounded-lg px-4 py-2 text-white"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Types -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-white">Post-Typen</h3>
|
||||
<button type="button" id="addPostTypeBtn" class="text-sm text-brand-highlight hover:underline">+ Post-Typ hinzufügen</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mb-4">Definiere verschiedene Arten von Posts (z.B. "Thought Leader", "Case Study", "How-To"). Diese werden zur Kategorisierung und typ-spezifischen Analyse verwendet.</p>
|
||||
|
||||
<div id="postTypesContainer" class="space-y-4">
|
||||
<!-- Post type entries will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Area -->
|
||||
<div id="progressArea" class="hidden">
|
||||
<div class="bg-brand-bg rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span id="progressMessage" class="text-gray-300">Starte Setup...</span>
|
||||
<span id="progressPercent" class="text-gray-400">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-brand-bg-dark rounded-full h-2">
|
||||
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Area -->
|
||||
<div id="resultArea" class="hidden">
|
||||
<div id="successResult" class="hidden bg-green-900/30 border border-green-500 rounded-lg p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
<span class="text-green-300">Setup erfolgreich abgeschlossen!</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="errorResult" class="hidden bg-red-900/30 border border-red-500 rounded-lg p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
<span id="errorMessage" class="text-red-300">Fehler beim Setup</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" id="submitBtn" class="flex-1 btn-primary font-medium py-3 rounded-lg transition-colors">
|
||||
Setup starten
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const form = document.getElementById('customerForm');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const progressArea = document.getElementById('progressArea');
|
||||
const resultArea = document.getElementById('resultArea');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressMessage = document.getElementById('progressMessage');
|
||||
const progressPercent = document.getElementById('progressPercent');
|
||||
const postTypesContainer = document.getElementById('postTypesContainer');
|
||||
const addPostTypeBtn = document.getElementById('addPostTypeBtn');
|
||||
|
||||
let postTypeIndex = 0;
|
||||
|
||||
function createPostTypeEntry() {
|
||||
const index = postTypeIndex++;
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'bg-brand-bg rounded-lg p-4 border border-brand-bg-light';
|
||||
entry.id = `postType_${index}`;
|
||||
entry.innerHTML = `
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<span class="text-sm font-medium text-gray-300">Post-Typ ${index + 1}</span>
|
||||
<button type="button" onclick="removePostType(${index})" class="text-red-400 hover:text-red-300 text-sm">Entfernen</button>
|
||||
</div>
|
||||
<div class="grid gap-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Name *</label>
|
||||
<input type="text" data-pt-field="name" data-pt-index="${index}" required placeholder="z.B. Thought Leader" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Beschreibung</label>
|
||||
<input type="text" data-pt-field="description" data-pt-index="${index}" placeholder="Kurze Beschreibung" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Identifizierende Hashtags (kommagetrennt)</label>
|
||||
<input type="text" data-pt-field="hashtags" data-pt-index="${index}" placeholder="#ThoughtLeader, #Insight, #Leadership" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Keywords (kommagetrennt)</label>
|
||||
<input type="text" data-pt-field="keywords" data-pt-index="${index}" placeholder="Erfahrung, Learnings, Meinung" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
|
||||
</div>
|
||||
<details class="mt-2">
|
||||
<summary class="text-xs text-gray-400 cursor-pointer hover:text-gray-300">Erweiterte Eigenschaften</summary>
|
||||
<div class="mt-3 grid gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Zweck</label>
|
||||
<input type="text" data-pt-field="purpose" data-pt-index="${index}" placeholder="z.B. Expertise zeigen, Meinungsführerschaft etablieren" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Typische Tonalität</label>
|
||||
<input type="text" data-pt-field="tone" data-pt-index="${index}" placeholder="z.B. reflektiert, provokativ, inspirierend" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Zielgruppe</label>
|
||||
<input type="text" data-pt-field="target_audience" data-pt-index="${index}" placeholder="z.B. Führungskräfte, Entscheider" class="w-full input-bg border rounded-lg px-3 py-2 text-white text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
`;
|
||||
postTypesContainer.appendChild(entry);
|
||||
}
|
||||
|
||||
function removePostType(index) {
|
||||
const entry = document.getElementById(`postType_${index}`);
|
||||
if (entry) {
|
||||
entry.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function collectPostTypes() {
|
||||
const postTypes = [];
|
||||
const entries = postTypesContainer.querySelectorAll('[id^="postType_"]');
|
||||
|
||||
entries.forEach(entry => {
|
||||
const index = entry.id.split('_')[1];
|
||||
const name = entry.querySelector(`[data-pt-field="name"][data-pt-index="${index}"]`)?.value?.trim();
|
||||
|
||||
if (name) {
|
||||
const hashtagsRaw = entry.querySelector(`[data-pt-field="hashtags"][data-pt-index="${index}"]`)?.value || '';
|
||||
const keywordsRaw = entry.querySelector(`[data-pt-field="keywords"][data-pt-index="${index}"]`)?.value || '';
|
||||
|
||||
postTypes.push({
|
||||
name: name,
|
||||
description: entry.querySelector(`[data-pt-field="description"][data-pt-index="${index}"]`)?.value?.trim() || null,
|
||||
identifying_hashtags: hashtagsRaw.split(',').map(h => h.trim()).filter(h => h),
|
||||
identifying_keywords: keywordsRaw.split(',').map(k => k.trim()).filter(k => k),
|
||||
semantic_properties: {
|
||||
purpose: entry.querySelector(`[data-pt-field="purpose"][data-pt-index="${index}"]`)?.value?.trim() || null,
|
||||
typical_tone: entry.querySelector(`[data-pt-field="tone"][data-pt-index="${index}"]`)?.value?.trim() || null,
|
||||
target_audience: entry.querySelector(`[data-pt-field="target_audience"][data-pt-index="${index}"]`)?.value?.trim() || null
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return postTypes;
|
||||
}
|
||||
|
||||
addPostTypeBtn.addEventListener('click', createPostTypeEntry);
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Wird gestartet...';
|
||||
progressArea.classList.remove('hidden');
|
||||
resultArea.classList.add('hidden');
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Add post types as JSON
|
||||
const postTypes = collectPostTypes();
|
||||
if (postTypes.length > 0) {
|
||||
formData.append('post_types_json', JSON.stringify(postTypes));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/customers', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Poll for progress
|
||||
const taskId = data.task_id;
|
||||
const pollInterval = setInterval(async () => {
|
||||
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
|
||||
const status = await statusResponse.json();
|
||||
|
||||
progressBar.style.width = `${status.progress}%`;
|
||||
progressPercent.textContent = `${status.progress}%`;
|
||||
progressMessage.textContent = status.message;
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
progressArea.classList.add('hidden');
|
||||
resultArea.classList.remove('hidden');
|
||||
document.getElementById('successResult').classList.remove('hidden');
|
||||
submitBtn.textContent = 'Setup starten';
|
||||
submitBtn.disabled = false;
|
||||
form.reset();
|
||||
postTypesContainer.innerHTML = '';
|
||||
postTypeIndex = 0;
|
||||
} else if (status.status === 'error') {
|
||||
clearInterval(pollInterval);
|
||||
progressArea.classList.add('hidden');
|
||||
resultArea.classList.remove('hidden');
|
||||
document.getElementById('errorResult').classList.remove('hidden');
|
||||
document.getElementById('errorMessage').textContent = status.message;
|
||||
submitBtn.textContent = 'Setup starten';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
progressArea.classList.add('hidden');
|
||||
resultArea.classList.remove('hidden');
|
||||
document.getElementById('errorResult').classList.remove('hidden');
|
||||
document.getElementById('errorMessage').textContent = error.message;
|
||||
submitBtn.textContent = 'Setup starten';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
1481
src/web/templates/admin/post_detail.html
Normal file
1481
src/web/templates/admin/post_detail.html
Normal file
File diff suppressed because it is too large
Load Diff
152
src/web/templates/admin/posts.html
Normal file
152
src/web/templates/admin/posts.html
Normal file
@@ -0,0 +1,152 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Alle Posts - LinkedIn Posts{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.post-card {
|
||||
background: linear-gradient(135deg, rgba(61, 72, 72, 0.3) 0%, rgba(45, 56, 56, 0.4) 100%);
|
||||
border: 1px solid rgba(61, 72, 72, 0.6);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.post-card:hover {
|
||||
border-color: rgba(255, 199, 0, 0.3);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.customer-header {
|
||||
background: linear-gradient(90deg, rgba(255, 199, 0, 0.1) 0%, transparent 100%);
|
||||
}
|
||||
.score-ring {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.score-high { background: rgba(34, 197, 94, 0.2); border: 2px solid rgba(34, 197, 94, 0.5); color: #86efac; }
|
||||
.score-medium { background: rgba(234, 179, 8, 0.2); border: 2px solid rgba(234, 179, 8, 0.5); color: #fde047; }
|
||||
.score-low { background: rgba(239, 68, 68, 0.2); border: 2px solid rgba(239, 68, 68, 0.5); color: #fca5a5; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Alle Posts</h1>
|
||||
<p class="text-gray-400">{{ total_posts }} generierte Posts</p>
|
||||
</div>
|
||||
<a href="/admin/create" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
Neuer Post
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
|
||||
<strong>Error:</strong> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if customers_with_posts %}
|
||||
<div class="space-y-8">
|
||||
{% for item in customers_with_posts %}
|
||||
{% if item.posts %}
|
||||
<div class="card-bg rounded-xl border overflow-hidden">
|
||||
<!-- Customer Header -->
|
||||
<div class="customer-header px-6 py-4 border-b border-brand-bg-light">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl flex items-center justify-center shadow-lg overflow-hidden {{ 'bg-brand-highlight' if not item.profile_picture else '' }}">
|
||||
{% if item.profile_picture %}
|
||||
<img src="{{ item.profile_picture }}" alt="{{ item.customer.name }}" class="w-full h-full object-cover" loading="lazy" referrerpolicy="no-referrer">
|
||||
{% else %}
|
||||
<span class="text-brand-bg-dark font-bold text-lg">{{ item.customer.name[0] | upper }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-white text-lg">{{ item.customer.name }}</h3>
|
||||
<p class="text-sm text-gray-400">{{ item.customer.company_name or 'Kein Unternehmen' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="px-4 py-1.5 bg-brand-bg rounded-full text-sm text-gray-300 font-medium">
|
||||
{{ item.post_count }} Post{{ 's' if item.post_count != 1 else '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts Grid -->
|
||||
<div class="p-4">
|
||||
<div class="grid gap-3">
|
||||
{% for post in item.posts %}
|
||||
<a href="/admin/posts/{{ post.id }}" class="post-card rounded-xl p-4 block group">
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Score Circle -->
|
||||
{% if post.critic_feedback and post.critic_feedback | length > 0 %}
|
||||
{% set score = post.critic_feedback[-1].overall_score %}
|
||||
<div class="score-ring flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">
|
||||
{{ score }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="score-ring flex-shrink-0 bg-brand-bg-dark border-2 border-brand-bg-light text-gray-500">
|
||||
—
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<h4 class="font-medium text-white group-hover:text-brand-highlight transition-colors truncate">
|
||||
{{ post.topic_title or 'Untitled' }}
|
||||
</h4>
|
||||
<span class="flex-shrink-0 px-2 py-0.5 text-xs rounded font-medium {{ 'bg-green-600/20 text-green-400 border border-green-600/30' if post.status == 'approved' else 'bg-yellow-600/20 text-yellow-400 border border-yellow-600/30' }}">
|
||||
{{ post.status | capitalize }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 mt-1.5 text-sm text-gray-500">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{{ post.created_at.strftime('%H:%M') if post.created_at else '' }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<svg class="w-5 h-5 text-gray-600 group-hover:text-brand-highlight transition-colors flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-bg rounded-xl border p-12 text-center">
|
||||
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Posts</h3>
|
||||
<p class="text-gray-400 mb-6 max-w-md mx-auto">Erstelle deinen ersten LinkedIn Post mit KI-Unterstützung.</p>
|
||||
<a href="/admin/create" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
Post erstellen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
215
src/web/templates/admin/research.html
Normal file
215
src/web/templates/admin/research.html
Normal file
@@ -0,0 +1,215 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Research Topics - LinkedIn Posts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Research Topics</h1>
|
||||
<p class="text-gray-400">Recherchiere neue Content-Themen mit Perplexity AI</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left: Form -->
|
||||
<div>
|
||||
<form id="researchForm" class="card-bg rounded-xl border p-6">
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
|
||||
<select name="customer_id" id="customerSelect" required class="w-full input-bg border rounded-lg px-4 py-3 text-white">
|
||||
<option value="">-- Kunde wählen --</option>
|
||||
{% for customer in customers %}
|
||||
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Post Type Selection -->
|
||||
<div id="postTypeArea" class="mb-6 hidden">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ (optional)</label>
|
||||
<div id="postTypeCards" class="grid grid-cols-2 gap-2 mb-2">
|
||||
<!-- Post type cards will be loaded here -->
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Wähle einen Post-Typ für gezielte Recherche oder lasse leer für allgemeine Recherche.</p>
|
||||
<input type="hidden" name="post_type_id" id="selectedPostTypeId" value="">
|
||||
</div>
|
||||
|
||||
<!-- Progress Area -->
|
||||
<div id="progressArea" class="hidden mb-6">
|
||||
<div class="bg-brand-bg rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span id="progressMessage" class="text-gray-300">Starte Recherche...</span>
|
||||
<span id="progressPercent" class="text-gray-400">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-brand-bg-dark rounded-full h-2">
|
||||
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="w-full btn-primary font-medium py-3 rounded-lg transition-colors flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||
Research starten
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if not customers %}
|
||||
<div class="mt-4 bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
|
||||
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/admin/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right: Results -->
|
||||
<div>
|
||||
<div id="resultsArea" class="card-bg rounded-xl border p-6">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Gefundene Topics</h3>
|
||||
<div id="topicsList" class="space-y-4">
|
||||
<p class="text-gray-400">Starte eine Recherche um Topics zu finden...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const form = document.getElementById('researchForm');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const progressArea = document.getElementById('progressArea');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressMessage = document.getElementById('progressMessage');
|
||||
const progressPercent = document.getElementById('progressPercent');
|
||||
const topicsList = document.getElementById('topicsList');
|
||||
const customerSelect = document.getElementById('customerSelect');
|
||||
const postTypeArea = document.getElementById('postTypeArea');
|
||||
const postTypeCards = document.getElementById('postTypeCards');
|
||||
const selectedPostTypeId = document.getElementById('selectedPostTypeId');
|
||||
|
||||
let currentPostTypes = [];
|
||||
|
||||
// Load post types when customer is selected
|
||||
customerSelect.addEventListener('change', async () => {
|
||||
const customerId = customerSelect.value;
|
||||
selectedPostTypeId.value = '';
|
||||
|
||||
if (!customerId) {
|
||||
postTypeArea.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/customers/${customerId}/post-types`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.post_types && data.post_types.length > 0) {
|
||||
currentPostTypes = data.post_types;
|
||||
postTypeArea.classList.remove('hidden');
|
||||
|
||||
postTypeCards.innerHTML = `
|
||||
<button type="button" onclick="selectPostType('')" id="pt_all"
|
||||
class="p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
|
||||
<div class="font-medium text-sm">Alle Typen</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Allgemeine Recherche</div>
|
||||
</button>
|
||||
` + data.post_types.map(pt => `
|
||||
<button type="button" onclick="selectPostType('${pt.id}')" id="pt_${pt.id}"
|
||||
class="p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
|
||||
<div class="font-medium text-sm">${pt.name}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">${pt.analyzed_post_count || 0} Posts analysiert</div>
|
||||
${pt.has_analysis ? '<span class="text-xs text-green-400">Analyse</span>' : ''}
|
||||
</button>
|
||||
`).join('');
|
||||
} else {
|
||||
postTypeArea.classList.add('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load post types:', error);
|
||||
postTypeArea.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
function selectPostType(typeId) {
|
||||
selectedPostTypeId.value = typeId;
|
||||
|
||||
// Update card styles
|
||||
document.querySelectorAll('[id^="pt_"]').forEach(card => {
|
||||
if (card.id === `pt_${typeId}` || (typeId === '' && card.id === 'pt_all')) {
|
||||
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-highlight/20 border-brand-highlight text-white';
|
||||
} else {
|
||||
card.className = 'p-3 rounded-lg border text-left transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const customerId = customerSelect.value;
|
||||
if (!customerId) return;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Recherchiert...';
|
||||
progressArea.classList.remove('hidden');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('customer_id', customerId);
|
||||
if (selectedPostTypeId.value) {
|
||||
formData.append('post_type_id', selectedPostTypeId.value);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/research', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
const taskId = data.task_id;
|
||||
const pollInterval = setInterval(async () => {
|
||||
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
|
||||
const status = await statusResponse.json();
|
||||
|
||||
progressBar.style.width = `${status.progress}%`;
|
||||
progressPercent.textContent = `${status.progress}%`;
|
||||
progressMessage.textContent = status.message;
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
progressArea.classList.add('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
|
||||
|
||||
// Display topics
|
||||
if (status.topics && status.topics.length > 0) {
|
||||
topicsList.innerHTML = status.topics.map((topic, i) => `
|
||||
<div class="bg-brand-bg rounded-lg p-4 border border-brand-bg-light">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<span class="inline-block px-2 py-1 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded mb-2">${topic.category || 'Topic'}</span>
|
||||
<h4 class="font-semibold text-white">${topic.title}</h4>
|
||||
${topic.angle ? `<p class="text-sm text-brand-highlight/80 mt-1">↳ ${topic.angle}</p>` : ''}
|
||||
${topic.hook_idea ? `<p class="text-sm text-gray-300 mt-2 italic border-l-2 border-brand-highlight/30 pl-2">"${topic.hook_idea.substring(0, 150)}..."</p>` : ''}
|
||||
<p class="text-gray-400 text-sm mt-2">${topic.fact ? topic.fact.substring(0, 200) + '...' : ''}</p>
|
||||
${topic.source ? `<p class="text-gray-500 text-xs mt-2">Quelle: ${topic.source}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
topicsList.innerHTML = '<p class="text-gray-400">Keine Topics gefunden.</p>';
|
||||
}
|
||||
} else if (status.status === 'error') {
|
||||
clearInterval(pollInterval);
|
||||
progressArea.classList.add('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
|
||||
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${status.message}</p>`;
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
progressArea.classList.add('hidden');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg> Research starten';
|
||||
topicsList.innerHTML = `<p class="text-red-400">Fehler: ${error.message}</p>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
571
src/web/templates/admin/scraped_posts.html
Normal file
571
src/web/templates/admin/scraped_posts.html
Normal file
@@ -0,0 +1,571 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Gescrapte Posts - LinkedIn Posts{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.post-card {
|
||||
background: linear-gradient(135deg, rgba(61, 72, 72, 0.3) 0%, rgba(45, 56, 56, 0.4) 100%);
|
||||
border: 1px solid rgba(61, 72, 72, 0.6);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.post-card:hover {
|
||||
border-color: rgba(255, 199, 0, 0.3);
|
||||
}
|
||||
.post-card.selected {
|
||||
border-color: rgba(255, 199, 0, 0.6);
|
||||
background: linear-gradient(135deg, rgba(255, 199, 0, 0.05) 0%, rgba(45, 56, 56, 0.4) 100%);
|
||||
}
|
||||
.type-badge {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.type-badge:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.type-badge.active {
|
||||
background-color: rgba(255, 199, 0, 0.2);
|
||||
border-color: #ffc700;
|
||||
}
|
||||
.post-content-preview {
|
||||
max-height: 150px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.post-content-preview::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: linear-gradient(transparent, rgba(45, 56, 56, 0.9));
|
||||
}
|
||||
.post-content-expanded {
|
||||
max-height: none;
|
||||
}
|
||||
.post-content-expanded::after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Gescrapte Posts verwalten</h1>
|
||||
<p class="text-gray-400">Posts manuell kategorisieren und Post-Typ-Analyse triggern</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Selection -->
|
||||
<div class="card-bg rounded-xl border p-6 mb-6">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div class="flex-1 min-w-64">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Kunde auswählen</label>
|
||||
<select id="customerSelect" class="w-full input-bg border rounded-lg px-4 py-3 text-white">
|
||||
<option value="">-- Kunde wählen --</option>
|
||||
{% for customer in customers %}
|
||||
<option value="{{ customer.id }}">{{ customer.name }} - {{ customer.company_name or 'Kein Unternehmen' }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button id="classifyAllBtn" class="hidden px-4 py-3 bg-brand-bg hover:bg-brand-bg-light border border-brand-bg-light rounded-lg text-white transition-colors">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Auto-Klassifizieren
|
||||
</span>
|
||||
</button>
|
||||
<button id="analyzeTypesBtn" class="hidden px-4 py-3 btn-primary rounded-lg font-medium transition-colors">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
|
||||
Post-Typen analysieren
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Area -->
|
||||
<div id="progressArea" class="hidden card-bg rounded-xl border p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span id="progressMessage" class="text-gray-300">Arbeite...</span>
|
||||
<span id="progressPercent" class="text-gray-400">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-brand-bg-dark rounded-full h-2">
|
||||
<div id="progressBar" class="bg-brand-highlight h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats & Post Types -->
|
||||
<div id="statsArea" class="hidden mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<div class="card-bg rounded-xl border p-4">
|
||||
<div class="text-2xl font-bold text-white" id="totalPostsCount">0</div>
|
||||
<div class="text-sm text-gray-400">Gesamt Posts</div>
|
||||
</div>
|
||||
<div class="card-bg rounded-xl border p-4">
|
||||
<div class="text-2xl font-bold text-green-400" id="classifiedCount">0</div>
|
||||
<div class="text-sm text-gray-400">Klassifiziert</div>
|
||||
</div>
|
||||
<div class="card-bg rounded-xl border p-4">
|
||||
<div class="text-2xl font-bold text-yellow-400" id="unclassifiedCount">0</div>
|
||||
<div class="text-sm text-gray-400">Nicht klassifiziert</div>
|
||||
</div>
|
||||
<div class="card-bg rounded-xl border p-4">
|
||||
<div class="text-2xl font-bold text-brand-highlight" id="postTypesCount">0</div>
|
||||
<div class="text-sm text-gray-400">Post-Typen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Type Filter -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<button onclick="filterByType(null)" id="filter_all" class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-highlight/20 border-brand-highlight text-white">
|
||||
Alle
|
||||
</button>
|
||||
<button onclick="filterByType('unclassified')" id="filter_unclassified" class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
|
||||
Nicht klassifiziert
|
||||
</button>
|
||||
<div id="postTypeFilters" class="contents">
|
||||
<!-- Post type filter buttons will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts List -->
|
||||
<div id="postsArea" class="hidden">
|
||||
<div id="postsList" class="space-y-4">
|
||||
<p class="text-gray-400">Lade Posts...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="emptyState" class="hidden card-bg rounded-xl border p-12 text-center">
|
||||
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-white mb-2">Keine gescrapten Posts</h3>
|
||||
<p class="text-gray-400 mb-6 max-w-md mx-auto">Für diesen Kunden wurden noch keine LinkedIn Posts gescrapet.</p>
|
||||
</div>
|
||||
|
||||
{% if not customers %}
|
||||
<div class="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4">
|
||||
<p class="text-yellow-300">Noch keine Kunden vorhanden. <a href="/admin/customers/new" class="underline">Erstelle zuerst einen Kunden</a>.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Post Detail Modal -->
|
||||
<div id="postModal" class="fixed inset-0 bg-black/70 hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-brand-bg-dark rounded-xl border border-brand-bg-light max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col shadow-2xl">
|
||||
<div class="p-4 border-b border-brand-bg-light flex items-center justify-between bg-brand-bg">
|
||||
<h3 class="text-lg font-semibold text-white">Post Details</h3>
|
||||
<button onclick="closeModal()" class="text-gray-400 hover:text-white p-1 hover:bg-brand-bg-light rounded">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto flex-1">
|
||||
<div id="modalContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const customerSelect = document.getElementById('customerSelect');
|
||||
const classifyAllBtn = document.getElementById('classifyAllBtn');
|
||||
const analyzeTypesBtn = document.getElementById('analyzeTypesBtn');
|
||||
const progressArea = document.getElementById('progressArea');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressMessage = document.getElementById('progressMessage');
|
||||
const progressPercent = document.getElementById('progressPercent');
|
||||
const statsArea = document.getElementById('statsArea');
|
||||
const postsArea = document.getElementById('postsArea');
|
||||
const postsList = document.getElementById('postsList');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const postTypeFilters = document.getElementById('postTypeFilters');
|
||||
const postModal = document.getElementById('postModal');
|
||||
const modalContent = document.getElementById('modalContent');
|
||||
|
||||
let currentPosts = [];
|
||||
let currentPostTypes = [];
|
||||
let currentFilter = null;
|
||||
|
||||
customerSelect.addEventListener('change', async () => {
|
||||
const customerId = customerSelect.value;
|
||||
|
||||
if (!customerId) {
|
||||
statsArea.classList.add('hidden');
|
||||
postsArea.classList.add('hidden');
|
||||
emptyState.classList.add('hidden');
|
||||
classifyAllBtn.classList.add('hidden');
|
||||
analyzeTypesBtn.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
await loadCustomerData(customerId);
|
||||
});
|
||||
|
||||
async function loadCustomerData(customerId) {
|
||||
// Load post types
|
||||
try {
|
||||
const ptResponse = await fetch(`/admin/api/customers/${customerId}/post-types`);
|
||||
const ptData = await ptResponse.json();
|
||||
currentPostTypes = ptData.post_types || [];
|
||||
|
||||
// Update post type filters
|
||||
postTypeFilters.innerHTML = currentPostTypes.map(pt => `
|
||||
<button onclick="filterByType('${pt.id}')" id="filter_${pt.id}"
|
||||
class="type-badge px-3 py-2 rounded-lg border text-sm transition-colors bg-brand-bg border-brand-bg-light hover:border-brand-highlight/50 text-white">
|
||||
${escapeHtml(pt.name)}
|
||||
<span class="ml-1 text-xs text-gray-400">(${pt.analyzed_post_count || 0})</span>
|
||||
${pt.has_analysis ? '<span class="ml-1 text-green-400">*</span>' : ''}
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('postTypesCount').textContent = currentPostTypes.length;
|
||||
} catch (error) {
|
||||
console.error('Failed to load post types:', error);
|
||||
}
|
||||
|
||||
// Load posts
|
||||
try {
|
||||
const response = await fetch(`/admin/api/customers/${customerId}/linkedin-posts`);
|
||||
const data = await response.json();
|
||||
|
||||
console.log('API Response:', data);
|
||||
|
||||
if (data.error) {
|
||||
console.error('API Error:', data.error);
|
||||
postsList.innerHTML = `<p class="text-red-400">API Fehler: ${escapeHtml(data.error)}</p>`;
|
||||
postsArea.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
currentPosts = data.posts || [];
|
||||
console.log(`Loaded ${currentPosts.length} posts`);
|
||||
|
||||
if (currentPosts.length === 0) {
|
||||
statsArea.classList.add('hidden');
|
||||
postsArea.classList.add('hidden');
|
||||
emptyState.classList.remove('hidden');
|
||||
classifyAllBtn.classList.add('hidden');
|
||||
analyzeTypesBtn.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update stats
|
||||
const classified = currentPosts.filter(p => p.post_type_id).length;
|
||||
const unclassified = currentPosts.length - classified;
|
||||
|
||||
document.getElementById('totalPostsCount').textContent = currentPosts.length;
|
||||
document.getElementById('classifiedCount').textContent = classified;
|
||||
document.getElementById('unclassifiedCount').textContent = unclassified;
|
||||
|
||||
statsArea.classList.remove('hidden');
|
||||
postsArea.classList.remove('hidden');
|
||||
emptyState.classList.add('hidden');
|
||||
classifyAllBtn.classList.remove('hidden');
|
||||
analyzeTypesBtn.classList.remove('hidden');
|
||||
|
||||
currentFilter = null;
|
||||
filterByType(null);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load posts:', error);
|
||||
postsList.innerHTML = `<p class="text-red-400">Fehler beim Laden: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function filterByType(typeId) {
|
||||
currentFilter = typeId;
|
||||
|
||||
// Update filter button styles
|
||||
document.querySelectorAll('.type-badge').forEach(btn => {
|
||||
const btnId = btn.id.replace('filter_', '');
|
||||
const isActive = (typeId === null && btnId === 'all') ||
|
||||
(typeId === 'unclassified' && btnId === 'unclassified') ||
|
||||
(btnId === typeId);
|
||||
|
||||
if (isActive) {
|
||||
btn.classList.add('active', 'bg-brand-highlight/20', 'border-brand-highlight');
|
||||
btn.classList.remove('bg-brand-bg', 'border-brand-bg-light');
|
||||
} else {
|
||||
btn.classList.remove('active', 'bg-brand-highlight/20', 'border-brand-highlight');
|
||||
btn.classList.add('bg-brand-bg', 'border-brand-bg-light');
|
||||
}
|
||||
});
|
||||
|
||||
// Filter posts
|
||||
let filteredPosts = currentPosts;
|
||||
if (typeId === 'unclassified') {
|
||||
filteredPosts = currentPosts.filter(p => !p.post_type_id);
|
||||
} else if (typeId) {
|
||||
filteredPosts = currentPosts.filter(p => p.post_type_id === typeId);
|
||||
}
|
||||
|
||||
renderPosts(filteredPosts);
|
||||
}
|
||||
|
||||
function renderPosts(posts) {
|
||||
if (posts.length === 0) {
|
||||
postsList.innerHTML = '<p class="text-gray-400 text-center py-8">Keine Posts in dieser Kategorie.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
postsList.innerHTML = posts.map((post, index) => {
|
||||
const postType = currentPostTypes.find(pt => pt.id === post.post_type_id);
|
||||
const postText = post.post_text || '';
|
||||
const previewText = postText.substring(0, 300);
|
||||
|
||||
return `
|
||||
<div class="post-card rounded-xl p-4 cursor-pointer" data-post-id="${post.id}" onclick="openPostModal('${post.id}')">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
||||
${postType ? `
|
||||
<span class="px-2 py-1 text-xs font-medium bg-brand-highlight/20 text-brand-highlight rounded">
|
||||
${escapeHtml(postType.name)}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">${post.classification_method || 'unknown'} (${Math.round((post.classification_confidence || 0) * 100)}%)</span>
|
||||
` : `
|
||||
<span class="px-2 py-1 text-xs font-medium bg-gray-600/30 text-gray-400 rounded">
|
||||
Nicht klassifiziert
|
||||
</span>
|
||||
`}
|
||||
${post.posted_at ? `
|
||||
<span class="text-xs text-gray-500">${new Date(post.posted_at).toLocaleDateString('de-DE')}</span>
|
||||
` : ''}
|
||||
${post.engagement_score ? `
|
||||
<span class="text-xs text-gray-500">Engagement: ${post.engagement_score}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Content Preview -->
|
||||
<div class="post-content-preview text-gray-300 text-sm whitespace-pre-wrap mb-3">
|
||||
${escapeHtml(previewText)}${postText.length > 300 ? '...' : ''}
|
||||
</div>
|
||||
|
||||
<!-- Click hint & Type badges -->
|
||||
<div class="flex items-center justify-between flex-wrap gap-2" onclick="event.stopPropagation()">
|
||||
<span class="text-xs text-gray-500 italic">Klicken für Vollansicht</span>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
${currentPostTypes.map(pt => `
|
||||
<button onclick="event.stopPropagation(); classifyPost('${post.id}', '${pt.id}')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${post.post_type_id === pt.id ? 'bg-brand-highlight/30 text-brand-highlight border border-brand-highlight' : 'bg-brand-bg hover:bg-brand-bg-light text-gray-300 border border-brand-bg-light'}">
|
||||
${escapeHtml(pt.name)}
|
||||
</button>
|
||||
`).join('')}
|
||||
${post.post_type_id ? `
|
||||
<button onclick="event.stopPropagation(); classifyPost('${post.id}', null)" class="px-2 py-1 text-xs rounded bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-900/50">
|
||||
✕
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function classifyPost(postId, postTypeId) {
|
||||
const customerId = customerSelect.value;
|
||||
if (!customerId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/linkedin-posts/${postId}/classify`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ post_type_id: postTypeId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to classify post');
|
||||
}
|
||||
|
||||
// Update local data
|
||||
const post = currentPosts.find(p => p.id === postId);
|
||||
if (post) {
|
||||
post.post_type_id = postTypeId;
|
||||
post.classification_method = 'manual';
|
||||
post.classification_confidence = 1.0;
|
||||
}
|
||||
|
||||
// Update stats
|
||||
const classified = currentPosts.filter(p => p.post_type_id).length;
|
||||
document.getElementById('classifiedCount').textContent = classified;
|
||||
document.getElementById('unclassifiedCount').textContent = currentPosts.length - classified;
|
||||
|
||||
// Re-render
|
||||
filterByType(currentFilter);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to classify post:', error);
|
||||
alert('Fehler beim Klassifizieren: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function openPostModal(postId) {
|
||||
const post = currentPosts.find(p => p.id === postId);
|
||||
if (!post) return;
|
||||
|
||||
const postType = currentPostTypes.find(pt => pt.id === post.post_type_id);
|
||||
|
||||
modalContent.innerHTML = `
|
||||
<div class="mb-4 flex items-center gap-3 flex-wrap">
|
||||
${postType ? `
|
||||
<span class="px-3 py-1.5 text-sm font-medium bg-brand-highlight/20 text-brand-highlight rounded-lg">
|
||||
${escapeHtml(postType.name)}
|
||||
</span>
|
||||
` : `
|
||||
<span class="px-3 py-1.5 text-sm font-medium bg-gray-600/30 text-gray-400 rounded-lg">
|
||||
Nicht klassifiziert
|
||||
</span>
|
||||
`}
|
||||
${post.posted_at ? `
|
||||
<span class="text-sm text-gray-500">${new Date(post.posted_at).toLocaleDateString('de-DE')}</span>
|
||||
` : ''}
|
||||
${post.engagement_score ? `
|
||||
<span class="text-sm text-gray-500">Engagement: ${post.engagement_score}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="bg-brand-bg rounded-xl p-6 mb-6 border border-brand-bg-light max-h-[50vh] overflow-y-auto">
|
||||
<div class="whitespace-pre-wrap text-gray-200 font-sans text-base leading-relaxed">${escapeHtml(post.post_text || '')}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-wrap border-t border-brand-bg-light pt-4">
|
||||
<span class="text-sm text-gray-400 font-medium">Typ zuweisen:</span>
|
||||
${currentPostTypes.map(pt => `
|
||||
<button onclick="classifyPost('${post.id}', '${pt.id}'); closeModal();"
|
||||
class="px-4 py-2 text-sm rounded-lg transition-colors ${post.post_type_id === pt.id ? 'bg-brand-highlight text-brand-bg-dark font-medium' : 'bg-brand-bg hover:bg-brand-bg-light text-gray-300 border border-brand-bg-light'}">
|
||||
${escapeHtml(pt.name)}
|
||||
</button>
|
||||
`).join('')}
|
||||
${post.post_type_id ? `
|
||||
<button onclick="classifyPost('${post.id}', null); closeModal();" class="px-4 py-2 text-sm rounded-lg bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-900/50">
|
||||
Klassifizierung entfernen
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
postModal.classList.remove('hidden');
|
||||
postModal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
postModal.classList.add('hidden');
|
||||
postModal.classList.remove('flex');
|
||||
}
|
||||
|
||||
// Close modal on backdrop click
|
||||
postModal.addEventListener('click', (e) => {
|
||||
if (e.target === postModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on Escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !postModal.classList.contains('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-classify button
|
||||
classifyAllBtn.addEventListener('click', async () => {
|
||||
const customerId = customerSelect.value;
|
||||
if (!customerId) return;
|
||||
|
||||
classifyAllBtn.disabled = true;
|
||||
progressArea.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/customers/${customerId}/classify-posts`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
const taskId = data.task_id;
|
||||
await pollTask(taskId, async () => {
|
||||
await loadCustomerData(customerId);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Classification failed:', error);
|
||||
alert('Fehler bei der Klassifizierung: ' + error.message);
|
||||
} finally {
|
||||
classifyAllBtn.disabled = false;
|
||||
progressArea.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze post types button
|
||||
analyzeTypesBtn.addEventListener('click', async () => {
|
||||
const customerId = customerSelect.value;
|
||||
if (!customerId) return;
|
||||
|
||||
analyzeTypesBtn.disabled = true;
|
||||
progressArea.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/customers/${customerId}/analyze-post-types`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
const taskId = data.task_id;
|
||||
await pollTask(taskId, async () => {
|
||||
await loadCustomerData(customerId);
|
||||
alert('Post-Typ-Analyse abgeschlossen! Die Analysen wurden aktualisiert.');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Analysis failed:', error);
|
||||
alert('Fehler bei der Analyse: ' + error.message);
|
||||
} finally {
|
||||
analyzeTypesBtn.disabled = false;
|
||||
progressArea.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
async function pollTask(taskId, onComplete) {
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusResponse = await fetch(`/admin/api/tasks/${taskId}`);
|
||||
const status = await statusResponse.json();
|
||||
|
||||
progressBar.style.width = `${status.progress}%`;
|
||||
progressPercent.textContent = `${status.progress}%`;
|
||||
progressMessage.textContent = status.message;
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(interval);
|
||||
await onComplete();
|
||||
resolve();
|
||||
} else if (status.status === 'error') {
|
||||
clearInterval(interval);
|
||||
alert('Fehler: ' + status.message);
|
||||
resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(interval);
|
||||
console.error('Polling error:', error);
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
159
src/web/templates/admin/status.html
Normal file
159
src/web/templates/admin/status.html
Normal file
@@ -0,0 +1,159 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Status - LinkedIn Posts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Status</h1>
|
||||
<p class="text-gray-400">Übersicht über alle Kunden und deren Setup-Status</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
|
||||
<strong>Error:</strong> {{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if customer_statuses %}
|
||||
<div class="grid gap-6">
|
||||
{% for item in customer_statuses %}
|
||||
<div class="card-bg rounded-xl border overflow-hidden">
|
||||
<!-- Customer Header -->
|
||||
<div class="px-6 py-4 border-b border-brand-bg-light">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center overflow-hidden {{ 'bg-brand-highlight' if not item.profile_picture else '' }}">
|
||||
{% if item.profile_picture %}
|
||||
<img src="{{ item.profile_picture }}" alt="{{ item.customer.name }}" class="w-full h-full object-cover" loading="lazy" referrerpolicy="no-referrer">
|
||||
{% else %}
|
||||
<span class="text-brand-bg-dark font-bold text-lg">{{ item.customer.name[0] | upper }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-white text-lg">{{ item.customer.name }}</h3>
|
||||
<p class="text-sm text-gray-400">{{ item.customer.company_name or 'Kein Unternehmen' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{% if item.status.ready_for_posts %}
|
||||
<span class="px-3 py-1.5 bg-green-600/30 text-green-300 rounded-lg text-sm font-medium flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
Bereit für Posts
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-3 py-1.5 bg-yellow-600/30 text-yellow-300 rounded-lg text-sm font-medium flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
Setup unvollständig
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Grid -->
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<!-- Scraped Posts -->
|
||||
<div class="bg-brand-bg/30 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
{% if item.status.has_scraped_posts %}
|
||||
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
{% else %}
|
||||
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
{% endif %}
|
||||
<span class="text-sm text-gray-400">Scraped Posts</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">{{ item.status.scraped_posts_count }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Profile Analysis -->
|
||||
<div class="bg-brand-bg/30 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
{% if item.status.has_profile_analysis %}
|
||||
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
{% else %}
|
||||
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
{% endif %}
|
||||
<span class="text-sm text-gray-400">Profil Analyse</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-white">{{ 'Vorhanden' if item.status.has_profile_analysis else 'Fehlt' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Research Topics -->
|
||||
<div class="bg-brand-bg/30 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
{% if item.status.research_count > 0 %}
|
||||
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
{% else %}
|
||||
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
{% endif %}
|
||||
<span class="text-sm text-gray-400">Research Topics</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">{{ item.status.research_count }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Generated Posts -->
|
||||
<div class="bg-brand-bg/30 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
<span class="text-sm text-gray-400">Generierte Posts</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-white">{{ item.status.posts_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Missing Items -->
|
||||
{% if item.status.missing_items %}
|
||||
<div class="bg-yellow-900/20 border border-yellow-600/50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-yellow-300 mb-2 flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
Fehlende Elemente
|
||||
</h4>
|
||||
<ul class="space-y-1">
|
||||
{% for item_missing in item.status.missing_items %}
|
||||
<li class="text-yellow-200/80 text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
{{ item_missing }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex flex-wrap gap-3 mt-4">
|
||||
{% if not item.status.has_profile_analysis %}
|
||||
<a href="/admin/customers/new" class="px-4 py-2 btn-primary rounded-lg text-sm transition-colors">
|
||||
Setup wiederholen
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if item.status.research_count == 0 %}
|
||||
<a href="/admin/research" class="px-4 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-sm text-white transition-colors">
|
||||
Recherche starten
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if item.status.ready_for_posts %}
|
||||
<a href="/admin/create" class="px-4 py-2 btn-primary rounded-lg text-sm transition-colors">
|
||||
Post erstellen
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/admin/impersonate/{{ item.customer.id }}" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg text-sm text-white transition-colors flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
|
||||
Als User einloggen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-bg rounded-xl border p-12 text-center">
|
||||
<svg class="w-16 h-16 text-gray-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
|
||||
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Kunden</h3>
|
||||
<p class="text-gray-400 mb-6">Erstelle deinen ersten Kunden, um den Status zu sehen.</p>
|
||||
<a href="/admin/customers/new" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
Neuer Kunde
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user