Major updates: LinkedIn auto-posting, timezone fixes, and Docker improvements

Features:
- Add LinkedIn OAuth integration and auto-posting functionality
- Add scheduler service for automated post publishing
- Add metadata field to generated_posts for LinkedIn URLs
- Add privacy policy page for LinkedIn API compliance
- Add company management features and employee accounts
- Add license key system for company registrations

Fixes:
- Fix timezone issues (use UTC consistently across app)
- Fix datetime serialization errors in database operations
- Fix scheduling timezone conversion (local time to UTC)
- Fix import errors (get_database -> db)

Infrastructure:
- Update Docker setup to use port 8001 (avoid conflicts)
- Add SSL support with nginx-proxy and Let's Encrypt
- Add LinkedIn setup documentation
- Add migration scripts for schema updates

Services:
- Add linkedin_service.py for LinkedIn API integration
- Add scheduler_service.py for background job processing
- Add storage_service.py for Supabase Storage
- Add email_service.py improvements
- Add encryption utilities for token storage

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 11:30:20 +01:00
parent b50594dbfa
commit f14515e9cf
94 changed files with 21601 additions and 5111 deletions

View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Onboarding{% endblock %} - 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; }
.card-bg { background-color: #4a5858; border-color: #5a6868; }
.input-bg { background-color: #3d4848; border-color: #5a6868; }
.input-bg:focus { border-color: #ffc700; outline: none; }
.btn-primary { background-color: #ffc700; color: #2d3838; }
.btn-primary:hover { background-color: #e6b300; }
.btn-secondary { background-color: #5a6868; color: #fff; }
.btn-secondary:hover { background-color: #6a7878; }
.step-active { background-color: #ffc700; color: #2d3838; }
.step-done { background-color: #22c55e; color: white; }
.step-pending { background-color: #5a6868; color: #999; }
</style>
{% block head %}{% endblock %}
</head>
<body class="text-gray-100 min-h-screen">
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- Header -->
<div class="text-center mb-8">
<img src="/static/logo.png" alt="Logo" class="h-12 w-auto mx-auto mb-4">
</div>
<!-- Progress Steps -->
{% if steps %}
<div class="mb-8">
<div class="flex items-center justify-center">
{% for step in steps %}
<div class="flex items-center">
<div class="flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium
{% if step.status == 'done' %}step-done
{% elif step.status == 'active' %}step-active
{% else %}step-pending{% endif %}">
{% if step.status == 'done' %}
<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>
{% else %}
{{ loop.index }}
{% endif %}
</div>
<span class="ml-2 text-sm {% if step.status == 'active' %}text-white{% else %}text-gray-500{% endif %}">
{{ step.name }}
</span>
</div>
{% if not loop.last %}
<div class="w-12 h-0.5 mx-2 {% if step.status == 'done' %}bg-green-500{% else %}bg-gray-600{% endif %}"></div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
<!-- Content Card -->
<div class="card-bg rounded-xl border p-8">
{% block content %}{% endblock %}
</div>
</div>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,193 @@
{% extends "onboarding/base.html" %}
{% block title %}Posts kategorisieren{% endblock %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-white mb-2">Posts kategorisieren</h1>
<p class="text-gray-400">Wir ordnen deine Posts automatisch den Post-Typen zu.</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">
{{ error }}
</div>
{% endif %}
<!-- Classification Progress -->
<div id="classification-section" class="mb-8">
<div class="card-bg border border-gray-600 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-white">Kategorisierung</h3>
<div id="classification-status" class="text-sm">
{% if classification_complete %}
<span class="text-green-400">Abgeschlossen</span>
{% else %}
<span class="text-yellow-400">Ausstehend</span>
{% endif %}
</div>
</div>
<!-- Progress Bar -->
<div class="mb-4">
<div class="h-2 bg-gray-600 rounded-full overflow-hidden">
<div id="progress-bar" class="h-full bg-brand-highlight transition-all duration-300"
style="width: {{ progress }}%"></div>
</div>
<div class="flex justify-between text-xs text-gray-400 mt-1">
<span id="progress-text">{{ classified_count }}/{{ total_posts }} Posts</span>
<span id="progress-percent">{{ progress }}%</span>
</div>
</div>
{% if not classification_complete %}
<button id="start-classification-btn"
class="btn-primary py-2 px-4 rounded-lg 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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
Automatisch kategorisieren
</button>
{% endif %}
</div>
</div>
<!-- Post Type Distribution -->
<div class="mb-8">
<h3 class="text-lg font-medium text-white mb-4">Verteilung nach Post-Typ</h3>
<div class="space-y-3" id="type-distribution">
{% for post_type in post_types %}
<div class="flex items-center gap-4">
<div class="flex-1">
<div class="flex justify-between text-sm mb-1">
<span class="text-white">{{ post_type.name }}</span>
<span class="text-gray-400">{{ post_type.count }} Posts</span>
</div>
<div class="h-2 bg-gray-600 rounded-full overflow-hidden">
<div class="h-full bg-brand-highlight"
style="width: {{ (post_type.count / total_posts * 100) if total_posts > 0 else 0 }}%"></div>
</div>
</div>
</div>
{% endfor %}
{% if uncategorized_count > 0 %}
<div class="flex items-center gap-4">
<div class="flex-1">
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-400">Nicht kategorisiert</span>
<span class="text-gray-400">{{ uncategorized_count }} Posts</span>
</div>
<div class="h-2 bg-gray-600 rounded-full overflow-hidden">
<div class="h-full bg-gray-500"
style="width: {{ (uncategorized_count / total_posts * 100) if total_posts > 0 else 0 }}%"></div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Manual Review Section -->
{% if uncategorized_posts and uncategorized_posts|length > 0 %}
<div class="mb-8">
<h3 class="text-lg font-medium text-white mb-4">Manuelle Nachkategorisierung</h3>
<p class="text-sm text-gray-400 mb-4">Diese Posts konnten nicht automatisch kategorisiert werden:</p>
<div class="space-y-4" id="manual-review-list">
{% for post in uncategorized_posts[:5] %}
<div class="bg-brand-bg border border-gray-600 rounded-lg p-4">
<p class="text-sm text-gray-300 mb-3 line-clamp-3">{{ post.post_text[:300] }}{% if post.post_text|length > 300 %}...{% endif %}</p>
<div class="flex flex-wrap gap-2">
{% for pt in post_types %}
<button type="button"
class="text-xs px-3 py-1 rounded-full border border-gray-500 text-gray-300 hover:border-brand-highlight hover:text-brand-highlight transition-colors"
onclick="categorizePost('{{ post.id }}', '{{ pt.id }}')">
{{ pt.name }}
</button>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Navigation -->
<form method="POST" action="/onboarding/categorize">
<div class="flex justify-between pt-6 border-t border-gray-600">
<a href="/onboarding/post-types" class="btn-secondary font-medium py-3 px-6 rounded-lg transition-colors">
Zurück
</a>
<button type="submit" class="btn-primary font-medium py-3 px-8 rounded-lg transition-colors"
{% if not classification_complete and classified_count < 3 %}disabled{% endif %}>
Weiter
</button>
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
const userId = "{{ user_id }}";
{% if not classification_complete %}
document.getElementById('start-classification-btn').addEventListener('click', async function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = `
<svg class="w-4 h-4 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 12h4z"></path>
</svg>
Kategorisiere...
`;
try {
const response = await fetch('/api/onboarding/classify-posts', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_id: userId})
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
btn.disabled = false;
btn.innerHTML = `
<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 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
Automatisch kategorisieren
`;
}
} catch (e) {
alert('Fehler bei der Kategorisierung');
btn.disabled = false;
}
});
{% endif %}
async function categorizePost(postId, postTypeId) {
try {
const response = await fetch('/api/onboarding/categorize-post', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({post_id: postId, post_type_id: postTypeId})
});
const data = await response.json();
if (data.success) {
location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch (e) {
alert('Fehler bei der Kategorisierung');
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,77 @@
{% extends "onboarding/base.html" %}
{% block title %}Unternehmensdaten{% endblock %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-white mb-2">Unternehmensdaten</h1>
<p class="text-gray-400">Erzähl uns mehr über dein Unternehmen.</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">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/onboarding/company" class="space-y-6">
<!-- Company Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-300 mb-1">
Unternehmensname *
</label>
<input type="text" id="name" name="name" required
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Dein Unternehmen GmbH"
value="{{ company.name if company else '' }}">
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium text-gray-300 mb-1">
Beschreibung
</label>
<textarea id="description" name="description" rows="3"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Was macht dein Unternehmen?">{{ company.description if company else '' }}</textarea>
</div>
<!-- Website & Industry -->
<div class="grid md:grid-cols-2 gap-4">
<div>
<label for="website" class="block text-sm font-medium text-gray-300 mb-1">
Website
</label>
<input type="url" id="website" name="website"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="https://dein-unternehmen.de"
value="{{ company.website if company else '' }}">
</div>
<div>
<label for="industry" class="block text-sm font-medium text-gray-300 mb-1">
Branche
</label>
<select id="industry" name="industry"
class="w-full input-bg border rounded-lg px-4 py-3 text-white">
<option value="">Bitte wählen...</option>
<option value="technology" {% if company and company.industry == 'technology' %}selected{% endif %}>Technologie</option>
<option value="finance" {% if company and company.industry == 'finance' %}selected{% endif %}>Finanzen</option>
<option value="healthcare" {% if company and company.industry == 'healthcare' %}selected{% endif %}>Gesundheitswesen</option>
<option value="education" {% if company and company.industry == 'education' %}selected{% endif %}>Bildung</option>
<option value="retail" {% if company and company.industry == 'retail' %}selected{% endif %}>Einzelhandel</option>
<option value="manufacturing" {% if company and company.industry == 'manufacturing' %}selected{% endif %}>Produktion</option>
<option value="consulting" {% if company and company.industry == 'consulting' %}selected{% endif %}>Beratung</option>
<option value="marketing" {% if company and company.industry == 'marketing' %}selected{% endif %}>Marketing & Werbung</option>
<option value="real_estate" {% if company and company.industry == 'real_estate' %}selected{% endif %}>Immobilien</option>
<option value="other" {% if company and company.industry == 'other' %}selected{% endif %}>Sonstiges</option>
</select>
</div>
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="btn-primary font-medium py-3 px-8 rounded-lg transition-colors">
Weiter zur Strategie
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{% extends "onboarding/base.html" %}
{% block title %}Setup abgeschlossen{% endblock %}
{% block content %}
<div class="text-center">
<!-- Success Icon -->
<div class="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 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>
</div>
<h1 class="text-2xl font-bold text-white mb-2">Setup abgeschlossen!</h1>
<p class="text-gray-400 mb-8">Dein Profil wurde erfolgreich eingerichtet. Du kannst jetzt mit dem Erstellen von Posts beginnen.</p>
<!-- Summary -->
<div class="bg-brand-bg border border-gray-600 rounded-lg p-6 mb-8 text-left">
<h3 class="text-lg font-medium text-white mb-4">Zusammenfassung</h3>
<div class="space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-gray-400">LinkedIn-Profil</span>
<span class="text-white">{{ profile.linkedin_url }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Posts analysiert</span>
<span class="text-white">{{ posts_count }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Post-Typen definiert</span>
<span class="text-white">{{ post_types_count }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Profil-Analyse</span>
{% if profile_analysis %}
<span class="text-green-400">Abgeschlossen</span>
{% elif analysis_started %}
<span class="text-brand-highlight animate-pulse">Läuft im Hintergrund...</span>
{% else %}
<span class="text-gray-500">Ausstehend</span>
{% endif %}
</div>
</div>
</div>
{% if analysis_started %}
<!-- Analysis Running Info -->
<div class="bg-brand-highlight/10 border border-brand-highlight/30 rounded-lg p-4 mb-8 text-left">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-brand-highlight mt-0.5 animate-spin" 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>
<div>
<h4 class="font-medium text-brand-highlight">Analyse läuft im Hintergrund</h4>
<p class="text-sm text-gray-400 mt-1">
Wir analysieren jetzt dein Profil, kategorisieren deine Posts und analysieren deine Post-Typen.
Du kannst das Dashboard bereits nutzen - die Fortschrittsanzeige siehst du unten rechts.
</p>
</div>
</div>
</div>
{% endif %}
<!-- Next Steps -->
<div class="grid md:grid-cols-3 gap-4 mb-8">
<div class="bg-brand-bg border border-gray-600 rounded-lg p-4">
<div class="w-10 h-10 bg-brand-highlight/20 rounded-lg flex items-center justify-center mb-3 mx-auto">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<h4 class="font-medium text-white mb-1">Topics recherchieren</h4>
<p class="text-xs text-gray-400">Finde relevante Themen für deine nächsten Posts</p>
</div>
<div class="bg-brand-bg border border-gray-600 rounded-lg p-4">
<div class="w-10 h-10 bg-brand-highlight/20 rounded-lg flex items-center justify-center mb-3 mx-auto">
<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="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>
<h4 class="font-medium text-white mb-1">Post erstellen</h4>
<p class="text-xs text-gray-400">Schreibe deinen ersten KI-unterstützten Post</p>
</div>
<div class="bg-brand-bg border border-gray-600 rounded-lg p-4">
<div class="w-10 h-10 bg-brand-highlight/20 rounded-lg flex items-center justify-center mb-3 mx-auto">
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
<h4 class="font-medium text-white mb-1">Einstellungen</h4>
<p class="text-xs text-gray-400">Passe dein Profil weiter an</p>
</div>
</div>
<!-- CTA -->
<a href="/" class="inline-block btn-primary font-medium py-3 px-8 rounded-lg transition-colors">
Zum Dashboard
</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,229 @@
{% extends "onboarding/base.html" %}
{% block title %}Post-Typen{% endblock %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-white mb-2">Post-Typen definieren</h1>
<p class="text-gray-400">Definiere die verschiedenen Arten von Posts, die du schreibst.</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">
{{ error }}
</div>
{% endif %}
<!-- Predefined Post Types -->
<div class="mb-8">
<h3 class="text-lg font-medium text-white mb-4">Vordefinierte Post-Typen</h3>
<p class="text-sm text-gray-400 mb-4">Wähle die Post-Typen aus, die zu deinem Content passen:</p>
<div class="grid md:grid-cols-2 gap-4" id="predefined-types">
<label class="flex items-start gap-3 p-4 border border-gray-600 rounded-lg cursor-pointer hover:border-brand-highlight transition-colors">
<input type="checkbox" name="predefined_type" value="thought_leadership" class="mt-1">
<div>
<span class="text-white font-medium">Thought Leadership</span>
<p class="text-sm text-gray-400">Brancheninsights, Meinungen, Trends</p>
</div>
</label>
<label class="flex items-start gap-3 p-4 border border-gray-600 rounded-lg cursor-pointer hover:border-brand-highlight transition-colors">
<input type="checkbox" name="predefined_type" value="personal_story" class="mt-1">
<div>
<span class="text-white font-medium">Personal Story</span>
<p class="text-sm text-gray-400">Persönliche Erfahrungen, Learnings</p>
</div>
</label>
<label class="flex items-start gap-3 p-4 border border-gray-600 rounded-lg cursor-pointer hover:border-brand-highlight transition-colors">
<input type="checkbox" name="predefined_type" value="how_to" class="mt-1">
<div>
<span class="text-white font-medium">How-To / Tutorial</span>
<p class="text-sm text-gray-400">Anleitungen, Tipps, Best Practices</p>
</div>
</label>
<label class="flex items-start gap-3 p-4 border border-gray-600 rounded-lg cursor-pointer hover:border-brand-highlight transition-colors">
<input type="checkbox" name="predefined_type" value="news_commentary" class="mt-1">
<div>
<span class="text-white font-medium">News & Kommentar</span>
<p class="text-sm text-gray-400">Aktuelle Nachrichten mit Einordnung</p>
</div>
</label>
<label class="flex items-start gap-3 p-4 border border-gray-600 rounded-lg cursor-pointer hover:border-brand-highlight transition-colors">
<input type="checkbox" name="predefined_type" value="case_study" class="mt-1">
<div>
<span class="text-white font-medium">Case Study</span>
<p class="text-sm text-gray-400">Erfolgsgeschichten, Projektberichte</p>
</div>
</label>
<label class="flex items-start gap-3 p-4 border border-gray-600 rounded-lg cursor-pointer hover:border-brand-highlight transition-colors">
<input type="checkbox" name="predefined_type" value="engagement" class="mt-1">
<div>
<span class="text-white font-medium">Engagement Post</span>
<p class="text-sm text-gray-400">Fragen, Umfragen, Diskussionen</p>
</div>
</label>
</div>
</div>
<!-- Custom Post Types -->
<div class="mb-8">
<h3 class="text-lg font-medium text-white mb-4">Eigene Post-Typen</h3>
<div id="custom-types-list" class="space-y-3 mb-4">
<!-- Custom types will be added here -->
</div>
<button type="button" id="add-custom-type-btn"
class="btn-secondary py-2 px-4 rounded-lg 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="M12 4v16m8-8H4"/>
</svg>
Eigenen Typ hinzufügen
</button>
</div>
<!-- Navigation -->
<form method="POST" action="/onboarding/post-types" id="post-types-form">
<input type="hidden" name="post_types_json" id="post-types-json">
<div class="flex justify-between pt-6 border-t border-gray-600">
<a href="/onboarding/posts" class="btn-secondary font-medium py-3 px-6 rounded-lg transition-colors">
Zurück
</a>
<button type="submit" class="btn-primary font-medium py-3 px-8 rounded-lg transition-colors">
Weiter
</button>
</div>
</form>
<!-- Custom Type Modal -->
<div id="custom-type-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="card-bg rounded-xl p-6 w-full max-w-md mx-4">
<h3 class="text-lg font-medium text-white mb-4">Eigenen Post-Typ erstellen</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Name *</label>
<input type="text" id="custom-type-name"
class="w-full input-bg border rounded-lg px-4 py-2 text-white"
placeholder="z.B. Produktvorstellung">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Beschreibung</label>
<textarea id="custom-type-description" rows="2"
class="w-full input-bg border rounded-lg px-4 py-2 text-white"
placeholder="Kurze Beschreibung..."></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Keywords (kommagetrennt)</label>
<input type="text" id="custom-type-keywords"
class="w-full input-bg border rounded-lg px-4 py-2 text-white"
placeholder="produkt, launch, neu">
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button type="button" onclick="closeCustomTypeModal()"
class="btn-secondary py-2 px-4 rounded-lg">Abbrechen</button>
<button type="button" onclick="addCustomType()"
class="btn-primary py-2 px-4 rounded-lg">Hinzufügen</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const customTypes = [];
// Predefined types mapping
const predefinedTypes = {
'thought_leadership': {name: 'Thought Leadership', description: 'Brancheninsights, Meinungen, Trends', keywords: ['insight', 'trend', 'meinung']},
'personal_story': {name: 'Personal Story', description: 'Persönliche Erfahrungen, Learnings', keywords: ['story', 'erfahrung', 'learning']},
'how_to': {name: 'How-To / Tutorial', description: 'Anleitungen, Tipps, Best Practices', keywords: ['howto', 'tipps', 'anleitung']},
'news_commentary': {name: 'News & Kommentar', description: 'Aktuelle Nachrichten mit Einordnung', keywords: ['news', 'aktuell', 'kommentar']},
'case_study': {name: 'Case Study', description: 'Erfolgsgeschichten, Projektberichte', keywords: ['case', 'projekt', 'erfolg']},
'engagement': {name: 'Engagement Post', description: 'Fragen, Umfragen, Diskussionen', keywords: ['frage', 'umfrage', 'diskussion']}
};
document.getElementById('add-custom-type-btn').addEventListener('click', function() {
document.getElementById('custom-type-modal').classList.remove('hidden');
});
function closeCustomTypeModal() {
document.getElementById('custom-type-modal').classList.add('hidden');
document.getElementById('custom-type-name').value = '';
document.getElementById('custom-type-description').value = '';
document.getElementById('custom-type-keywords').value = '';
}
function addCustomType() {
const name = document.getElementById('custom-type-name').value.trim();
const description = document.getElementById('custom-type-description').value.trim();
const keywords = document.getElementById('custom-type-keywords').value.split(',').map(k => k.trim()).filter(k => k);
if (!name) {
alert('Name ist erforderlich');
return;
}
customTypes.push({name, description, keywords, custom: true});
renderCustomTypes();
closeCustomTypeModal();
}
function removeCustomType(index) {
customTypes.splice(index, 1);
renderCustomTypes();
}
function renderCustomTypes() {
const container = document.getElementById('custom-types-list');
container.innerHTML = customTypes.map((type, index) => `
<div class="flex items-center justify-between p-3 bg-brand-bg border border-gray-600 rounded-lg">
<div>
<span class="text-white font-medium">${type.name}</span>
${type.description ? `<p class="text-sm text-gray-400">${type.description}</p>` : ''}
</div>
<button type="button" onclick="removeCustomType(${index})" class="text-gray-500 hover:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
`).join('');
}
// Form submission
document.getElementById('post-types-form').addEventListener('submit', function(e) {
// Collect selected predefined types
const selected = Array.from(document.querySelectorAll('input[name="predefined_type"]:checked'))
.map(cb => {
const def = predefinedTypes[cb.value];
return {
name: def.name,
description: def.description,
identifying_keywords: def.keywords,
custom: false
};
});
// Add custom types
const allTypes = [...selected, ...customTypes.map(t => ({
name: t.name,
description: t.description,
identifying_keywords: t.keywords,
custom: true
}))];
document.getElementById('post-types-json').value = JSON.stringify(allTypes);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,226 @@
{% extends "onboarding/base.html" %}
{% block title %}Posts analysieren{% endblock %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-white mb-2">Posts analysieren</h1>
<p class="text-gray-400">Wir analysieren die bisherigen LinkedIn-Posts deines Kunden, um den Schreibstil zu lernen.</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">
{{ error }}
</div>
{% endif %}
<!-- Scraping Status -->
<div id="scraping-section" class="mb-8">
<div class="card-bg border border-gray-600 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-medium text-white">LinkedIn Posts von {{ profile.display_name }}</h3>
<p class="text-sm text-gray-400">Profil: {{ profile.linkedin_url }}</p>
</div>
<div id="post-count" class="text-2xl font-bold text-brand-highlight">
<span id="post-count-number">{{ scraped_posts_count }}</span> Posts
</div>
</div>
<!-- Scraping in Progress Notice -->
<div id="scraping-progress" class="{% if not scraping_in_progress %}hidden{% endif %} bg-brand-highlight/10 border border-brand-highlight/30 rounded-lg p-4 mb-4">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-brand-highlight animate-spin" 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>
<div>
<p class="text-brand-highlight font-medium">Posts werden im Hintergrund geladen...</p>
<p class="text-sm text-gray-400">Du kannst fortfahren sobald genug Posts vorhanden sind.</p>
</div>
</div>
</div>
{% if scraped_posts_count < 10 %}
<div id="low-posts-warning" class="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-yellow-500 mt-0.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>
<div>
<p class="text-yellow-200 font-medium">Zu wenige Posts gefunden</p>
<p class="text-yellow-300/70 text-sm">Für eine gute Stilanalyse brauchen wir mindestens 10 Posts. Du kannst entweder manuell Beispiel-Posts hinzufügen oder warten bis das Scraping fertig ist.</p>
</div>
</div>
</div>
{% endif %}
<button id="rescrape-btn" class="btn-secondary py-2 px-4 rounded-lg 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="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>
<span>Posts neu laden</span>
</button>
</div>
</div>
<!-- Manual Posts Section -->
<div id="manual-posts-section" class="{% if scraped_posts_count >= 10 %}hidden{% endif %} mb-8">
<h3 class="text-lg font-medium text-white mb-4">Beispiel-Posts manuell hinzufügen</h3>
<form id="manual-post-form" class="space-y-4">
<div>
<textarea id="manual-post-text" rows="6"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Füge hier einen LinkedIn-Post deines Kunden ein..."></textarea>
</div>
<button type="submit" class="btn-secondary py-2 px-4 rounded-lg transition-colors">
Post hinzufügen
</button>
</form>
<!-- Added Manual Posts -->
<div id="manual-posts-list" class="mt-4 space-y-2">
{% for post in example_posts %}
<div class="bg-brand-bg border border-gray-600 rounded-lg p-3 flex justify-between items-start">
<p class="text-sm text-gray-300 line-clamp-2">{{ post.post_text[:200] }}{% if post.post_text|length > 200 %}...{% endif %}</p>
<button class="text-gray-500 hover:text-red-400 ml-2" onclick="removeManualPost('{{ post.id }}', this.parentElement)">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{% endfor %}
</div>
</div>
<!-- Navigation -->
<div class="flex justify-between pt-8 border-t border-gray-600">
<a href="/onboarding/profile" class="text-gray-400 hover:text-white transition-colors 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="M11 17l-5-5m0 0l5-5m-5 5h12"/>
</svg>
Zurück
</a>
<form method="POST" action="/onboarding/posts">
<button type="submit" id="continue-btn" class="btn-primary font-medium py-3 px-8 rounded-lg transition-colors flex items-center gap-2"
{% if scraped_posts_count < 5 %}disabled{% endif %}>
Weiter
<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="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
</button>
</form>
</div>
<script>
async function removeManualPost(postId, element) {
try {
const response = await fetch(`/api/onboarding/remove-manual-post/${postId}`, {method: 'DELETE'});
if (response.ok) {
if (element) {
element.remove();
} else {
// Find and remove the element by post ID
const btn = document.querySelector(`[onclick*="${postId}"]`);
if (btn) btn.closest('.bg-brand-bg').remove();
}
}
} catch (e) {
console.error('Error removing manual post:', e);
}
}
document.addEventListener('DOMContentLoaded', function() {
const postCountEl = document.getElementById('post-count-number');
const progressEl = document.getElementById('scraping-progress');
const warningEl = document.getElementById('low-posts-warning');
const manualSection = document.getElementById('manual-posts-section');
const continueBtn = document.getElementById('continue-btn');
const rescrapeBtn = document.getElementById('rescrape-btn');
// Poll for post count updates
let pollInterval = setInterval(async () => {
try {
const response = await fetch('/api/posts-count');
const data = await response.json();
postCountEl.textContent = data.count;
// Update UI based on count
if (data.count >= 10) {
if (warningEl) warningEl.classList.add('hidden');
if (manualSection) manualSection.classList.add('hidden');
}
if (data.count >= 5) {
continueBtn.disabled = false;
}
// Check if scraping is still in progress
if (!data.scraping_active) {
progressEl.classList.add('hidden');
}
} catch (e) {
console.error('Error polling post count:', e);
}
}, 3000);
// Manual post form
const form = document.getElementById('manual-post-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const textarea = document.getElementById('manual-post-text');
const text = textarea.value.trim();
if (!text) return;
try {
const response = await fetch('/api/onboarding/add-manual-post', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({post_text: text})
});
if (response.ok) {
const data = await response.json();
textarea.value = '';
// Add to list
const list = document.getElementById('manual-posts-list');
const div = document.createElement('div');
div.className = 'bg-brand-bg border border-gray-600 rounded-lg p-3 flex justify-between items-start';
div.innerHTML = `
<p class="text-sm text-gray-300 line-clamp-2">${text.substring(0, 200)}${text.length > 200 ? '...' : ''}</p>
<button class="text-gray-500 hover:text-red-400 ml-2" onclick="removeManualPost('${data.id}', this.parentElement)">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
list.appendChild(div);
}
} catch (e) {
console.error('Error adding manual post:', e);
}
});
// Rescrape button
rescrapeBtn.addEventListener('click', async () => {
try {
const response = await fetch('/api/onboarding/rescrape', {method: 'POST'});
if (response.ok) {
progressEl.classList.remove('hidden');
showToast('Scraping gestartet...');
}
} catch (e) {
console.error('Error starting rescrape:', e);
}
});
// Cleanup on leave
window.addEventListener('beforeunload', () => {
clearInterval(pollInterval);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,130 @@
{% extends "onboarding/base.html" %}
{% block title %}Profil einrichten{% endblock %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-white mb-2">Profil einrichten</h1>
<p class="text-gray-400">Richte dein Ghostwriter-Profil ein und gib die Daten deines Kunden an.</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">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/onboarding/profile" class="space-y-8">
<!-- Section: Ghostwriter (You) -->
<div class="bg-brand-bg border border-gray-600 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
Deine Daten (Ghostwriter)
</h2>
<p class="text-gray-400 text-sm mb-4">Diese Informationen werden in deinem Dashboard angezeigt.</p>
<div class="grid md:grid-cols-2 gap-4">
<div>
<label for="ghostwriter_name" class="block text-sm font-medium text-gray-300 mb-1">
Dein Name *
</label>
<input type="text" id="ghostwriter_name" name="ghostwriter_name" required
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Max Mustermann"
value="{{ prefill.ghostwriter_name or session.linkedin_name or '' }}">
</div>
<div>
<label for="creator_email" class="block text-sm font-medium text-gray-300 mb-1">
Deine E-Mail
</label>
<input type="email" id="creator_email" name="creator_email"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="ghostwriter@email.de"
value="{{ prefill.creator_email or session.email or '' }}">
<p class="text-xs text-gray-500 mt-1">Du wirst über Entscheidungen benachrichtigt</p>
</div>
</div>
</div>
<!-- Section: Client/Customer -->
<div class="bg-brand-bg border border-gray-600 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-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="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 0z"/>
</svg>
Kunden-Daten (für wen du schreibst)
</h2>
<p class="text-gray-400 text-sm mb-4">Die Person/Marke, für die du LinkedIn-Posts erstellst.</p>
<div class="space-y-4">
<div class="grid md:grid-cols-2 gap-4">
<div>
<label for="customer_name" class="block text-sm font-medium text-gray-300 mb-1">
Kunden-Name *
</label>
<input type="text" id="customer_name" name="customer_name" required
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Lisa Kundin"
value="{{ prefill.customer_name or '' }}">
</div>
<div>
<label for="customer_email" class="block text-sm font-medium text-gray-300 mb-1">
Kunden E-Mail
</label>
<input type="email" id="customer_email" name="customer_email"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="kunde@email.de"
value="{{ prefill.customer_email or '' }}">
<p class="text-xs text-gray-500 mt-1">Genehmigt Posts per E-Mail (optional)</p>
</div>
</div>
<div>
<label for="linkedin_url" class="block text-sm font-medium text-gray-300 mb-1">
LinkedIn-Profil des Kunden *
</label>
<input type="url" id="linkedin_url" name="linkedin_url" required
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="https://linkedin.com/in/kunde-name"
value="{{ prefill.linkedin_url or '' }}">
<p class="text-xs text-gray-500 mt-1">Wir analysieren die bisherigen Posts, um den Schreibstil zu lernen</p>
</div>
<div>
<label for="company_name" class="block text-sm font-medium text-gray-300 mb-1">
Firma des Kunden (optional)
</label>
<input type="text" id="company_name" name="company_name"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Kunden GmbH"
value="{{ prefill.company_name or '' }}">
</div>
</div>
</div>
<!-- Section: Writing Style Notes -->
<div class="bg-brand-bg border border-gray-600 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-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="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>
Schreibstil-Notizen (optional)
</h2>
<textarea id="writing_style_notes" name="writing_style_notes" rows="4"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Besondere Hinweise zum Schreibstil des Kunden, z.B. 'Duzt immer', 'Nutzt oft Emojis', 'Formeller Ton', etc.">{{ prefill.writing_style_notes or '' }}</textarea>
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="btn-primary font-medium py-3 px-8 rounded-lg transition-colors flex items-center gap-2">
Weiter
<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="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% extends "onboarding/base.html" %}
{% block title %}Profil einrichten{% endblock %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-white mb-2">Profil einrichten</h1>
<p class="text-gray-400">Richte dein LinkedIn-Profil ein, um personalisierte Posts zu erstellen.</p>
{% if session.company_name %}
<p class="text-brand-highlight text-sm mt-2">Du wirst als Mitarbeiter von {{ session.company_name }} registriert.</p>
{% endif %}
</div>
{% if error %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/onboarding/profile" class="space-y-8">
<!-- Hidden field to indicate employee flow -->
<input type="hidden" name="is_employee" value="true">
<!-- Section: Your Profile -->
<div class="bg-brand-bg border border-gray-600 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
Deine Daten
</h2>
<p class="text-gray-400 text-sm mb-4">Diese Informationen werden in deinem Dashboard angezeigt.</p>
<div class="space-y-4">
<div class="grid md:grid-cols-2 gap-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-300 mb-1">
Dein Name *
</label>
<input type="text" id="name" name="name" required
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Max Mustermann"
value="{{ prefill.name or session.linkedin_name or '' }}">
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-300 mb-1">
Deine E-Mail
</label>
<input type="email" id="email" name="email"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="deine@email.de"
value="{{ prefill.email or session.email or '' }}">
<p class="text-xs text-gray-500 mt-1">Du wirst benachrichtigt, wenn Posts bereit sind</p>
</div>
</div>
<div>
<label for="linkedin_url" class="block text-sm font-medium text-gray-300 mb-1">
Dein LinkedIn-Profil *
</label>
<input type="url" id="linkedin_url" name="linkedin_url" required
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="https://linkedin.com/in/dein-name"
value="{{ prefill.linkedin_url or '' }}">
<p class="text-xs text-gray-500 mt-1">Wir analysieren deine bisherigen Posts, um deinen Schreibstil zu lernen</p>
</div>
</div>
</div>
<!-- Section: Writing Style Notes -->
<div class="bg-brand-bg border border-gray-600 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-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="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>
Schreibstil-Notizen (optional)
</h2>
<textarea id="writing_style_notes" name="writing_style_notes" rows="4"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Besondere Hinweise zu deinem Schreibstil, z.B. 'Ich duze immer', 'Nutze oft Emojis', 'Formeller Ton', etc.">{{ prefill.writing_style_notes or '' }}</textarea>
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="btn-primary font-medium py-3 px-8 rounded-lg transition-colors flex items-center gap-2">
Weiter
<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="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,277 @@
{% extends "onboarding/base.html" %}
{% block title %}Unternehmensstrategie{% endblock %}
{% block content %}
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-white mb-2">Unternehmensstrategie</h1>
<p class="text-gray-400">Definiere die Content-Strategie für dein Unternehmen. Diese Richtlinien werden bei jedem Post berücksichtigt.</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">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/onboarding/strategy" class="space-y-8">
<!-- Mission & Vision -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-white">Mission & Vision</h3>
<div>
<label for="mission" class="block text-sm font-medium text-gray-300 mb-1">
Mission
</label>
<textarea id="mission" name="mission" rows="2"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Was ist der Zweck deines Unternehmens?">{{ strategy.mission if strategy else '' }}</textarea>
</div>
<div>
<label for="vision" class="block text-sm font-medium text-gray-300 mb-1">
Vision
</label>
<textarea id="vision" name="vision" rows="2"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Wo soll dein Unternehmen in 5-10 Jahren stehen?">{{ strategy.vision if strategy else '' }}</textarea>
</div>
</div>
<!-- Brand Voice -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-white">Brand Voice & Tonalität</h3>
<div>
<label for="brand_voice" class="block text-sm font-medium text-gray-300 mb-1">
Brand Voice
</label>
<textarea id="brand_voice" name="brand_voice" rows="2"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Wie soll dein Unternehmen klingen? z.B. 'professionell aber nahbar', 'innovativ und zukunftsorientiert'">{{ strategy.brand_voice if strategy else '' }}</textarea>
</div>
<div>
<label for="tone_guidelines" class="block text-sm font-medium text-gray-300 mb-1">
Tonalitäts-Richtlinien
</label>
<textarea id="tone_guidelines" name="tone_guidelines" rows="3"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Spezifische Anweisungen zur Tonalität, z.B. 'Wir duzen unsere Zielgruppe', 'Wir nutzen keine Anglizismen'">{{ strategy.tone_guidelines if strategy else '' }}</textarea>
</div>
</div>
<!-- Content Pillars -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-white">Content Pillars</h3>
<p class="text-sm text-gray-400">Die Hauptthemen, über die dein Unternehmen kommuniziert (max. 5).</p>
<div id="content-pillars" class="space-y-2">
{% if strategy and strategy.content_pillars %}
{% for pillar in strategy.content_pillars %}
<div class="flex gap-2 pillar-row">
<input type="text" name="content_pillar" value="{{ pillar }}"
class="flex-1 input-bg border rounded-lg px-4 py-2 text-white"
placeholder="z.B. 'Digitale Transformation'">
<button type="button" onclick="removePillar(this)"
class="text-gray-500 hover:text-red-400 px-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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{% endfor %}
{% else %}
<div class="flex gap-2 pillar-row">
<input type="text" name="content_pillar"
class="flex-1 input-bg border rounded-lg px-4 py-2 text-white"
placeholder="z.B. 'Digitale Transformation'">
<button type="button" onclick="removePillar(this)"
class="text-gray-500 hover:text-red-400 px-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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{% endif %}
</div>
<button type="button" onclick="addPillar()"
class="text-sm text-brand-highlight hover:underline flex items-center gap-1">
<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 4v16m8-8H4"/>
</svg>
Weiteren Pillar hinzufügen
</button>
</div>
<!-- Target Audience -->
<div>
<label for="target_audience" class="block text-sm font-medium text-gray-300 mb-1">
Zielgruppe
</label>
<textarea id="target_audience" name="target_audience" rows="2"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Wer ist eure Zielgruppe auf LinkedIn? z.B. 'B2B Entscheider in der DACH-Region, CEOs und CTOs von mittelstaendischen Unternehmen'">{{ strategy.target_audience if strategy else '' }}</textarea>
</div>
<!-- Do's and Don'ts -->
<div class="grid md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">
Do's (empfohlen)
</label>
<div id="dos-list" class="space-y-2 mb-2">
{% if strategy and strategy.dos %}
{% for item in strategy.dos %}
<div class="flex gap-2 do-row">
<input type="text" name="do_item" value="{{ item }}"
class="flex-1 input-bg border rounded-lg px-3 py-2 text-white text-sm"
placeholder="z.B. 'Aktuelle Studien zitieren'">
<button type="button" onclick="removeItem(this)"
class="text-gray-500 hover:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{% endfor %}
{% else %}
<div class="flex gap-2 do-row">
<input type="text" name="do_item"
class="flex-1 input-bg border rounded-lg px-3 py-2 text-white text-sm"
placeholder="z.B. 'Aktuelle Studien zitieren'">
<button type="button" onclick="removeItem(this)"
class="text-gray-500 hover:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{% endif %}
</div>
<button type="button" onclick="addDo()"
class="text-xs text-brand-highlight hover:underline">+ Hinzufügen</button>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">
Don'ts (vermeiden)
</label>
<div id="donts-list" class="space-y-2 mb-2">
{% if strategy and strategy.donts %}
{% for item in strategy.donts %}
<div class="flex gap-2 dont-row">
<input type="text" name="dont_item" value="{{ item }}"
class="flex-1 input-bg border rounded-lg px-3 py-2 text-white text-sm"
placeholder="z.B. 'Keine politischen Themen'">
<button type="button" onclick="removeItem(this)"
class="text-gray-500 hover:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{% endfor %}
{% else %}
<div class="flex gap-2 dont-row">
<input type="text" name="dont_item"
class="flex-1 input-bg border rounded-lg px-3 py-2 text-white text-sm"
placeholder="z.B. 'Keine politischen Themen'">
<button type="button" onclick="removeItem(this)"
class="text-gray-500 hover:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{% endif %}
</div>
<button type="button" onclick="addDont()"
class="text-xs text-brand-highlight hover:underline">+ Hinzufügen</button>
</div>
</div>
<div class="flex justify-between pt-6 border-t border-gray-600">
<a href="/onboarding/company" class="btn-secondary font-medium py-3 px-6 rounded-lg transition-colors">
Zurück
</a>
<button type="submit" class="btn-primary font-medium py-3 px-8 rounded-lg transition-colors">
Strategie speichern
</button>
</div>
</form>
{% endblock %}
{% block scripts %}
<script>
function addPillar() {
const container = document.getElementById('content-pillars');
if (container.querySelectorAll('.pillar-row').length >= 5) {
alert('Maximal 5 Content Pillars erlaubt');
return;
}
const row = document.createElement('div');
row.className = 'flex gap-2 pillar-row';
row.innerHTML = `
<input type="text" name="content_pillar"
class="flex-1 input-bg border rounded-lg px-4 py-2 text-white"
placeholder="z.B. 'Digitale Transformation'">
<button type="button" onclick="removePillar(this)"
class="text-gray-500 hover:text-red-400 px-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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(row);
}
function removePillar(btn) {
const container = document.getElementById('content-pillars');
if (container.querySelectorAll('.pillar-row').length > 1) {
btn.closest('.pillar-row').remove();
}
}
function addDo() {
const container = document.getElementById('dos-list');
const row = document.createElement('div');
row.className = 'flex gap-2 do-row';
row.innerHTML = `
<input type="text" name="do_item"
class="flex-1 input-bg border rounded-lg px-3 py-2 text-white text-sm"
placeholder="z.B. 'Aktuelle Studien zitieren'">
<button type="button" onclick="removeItem(this)"
class="text-gray-500 hover:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(row);
}
function addDont() {
const container = document.getElementById('donts-list');
const row = document.createElement('div');
row.className = 'flex gap-2 dont-row';
row.innerHTML = `
<input type="text" name="dont_item"
class="flex-1 input-bg border rounded-lg px-3 py-2 text-white text-sm"
placeholder="z.B. 'Keine politischen Themen'">
<button type="button" onclick="removeItem(this)"
class="text-gray-500 hover:text-red-400">
<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="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
`;
container.appendChild(row);
}
function removeItem(btn) {
btn.closest('div').remove();
}
</script>
{% endblock %}