major restructure + Permission system

This commit is contained in:
2026-02-20 13:33:51 +01:00
parent c1960932a2
commit 10b07d89ac
15 changed files with 3148 additions and 131 deletions

View File

@@ -1640,6 +1640,45 @@ class DatabaseClient:
) )
return [LinkedInAccount(**row) for row in result.data] return [LinkedInAccount(**row) for row in result.data]
# ==================== EMPLOYEE COMPANY PERMISSIONS ====================
async def get_employee_permissions(self, user_id: UUID, company_id: UUID):
"""Get permissions for an employee in a company. Returns None if no row exists (treat as all-true defaults)."""
from src.database.models import EmployeeCompanyPermissions
result = await asyncio.to_thread(
lambda: self.client.table("employee_company_permissions").select("*").eq(
"user_id", str(user_id)
).eq("company_id", str(company_id)).execute()
)
if result.data:
return EmployeeCompanyPermissions(**result.data[0])
return None
async def upsert_employee_permissions(self, user_id: UUID, company_id: UUID, updates: Dict[str, Any]) -> None:
"""Insert or update employee-company permissions."""
data = {
"user_id": str(user_id),
"company_id": str(company_id),
**updates
}
await asyncio.to_thread(
lambda: self.client.table("employee_company_permissions").upsert(
data, on_conflict="user_id,company_id"
).execute()
)
logger.info(f"Upserted permissions for user {user_id} in company {company_id}")
async def get_scheduled_posts_for_user(self, user_id: UUID) -> List[GeneratedPost]:
"""Get scheduled/approved/published posts for an employee (for their calendar)."""
result = await asyncio.to_thread(
lambda: self.client.table("generated_posts").select("*").eq(
"user_id", str(user_id)
).in_(
"status", ["approved", "ready", "scheduled", "published"]
).order("scheduled_at", desc=False, nullsfirst=False).execute()
)
return [GeneratedPost(**item) for item in result.data]
# Global database client instance # Global database client instance
db = DatabaseClient() db = DatabaseClient()

View File

@@ -415,3 +415,19 @@ class CompanyDailyQuota(DBModel):
tokens_used: int = 0 tokens_used: int = 0
created_at: Optional[datetime] = None created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None
class EmployeeCompanyPermissions(DBModel):
"""Permissions granted by an employee to their company."""
id: Optional[UUID] = None
user_id: UUID
company_id: UUID
can_create_posts: bool = True
can_view_posts: bool = True
can_edit_posts: bool = True
can_schedule_posts: bool = True
can_manage_post_types: bool = True
can_do_research: bool = True
can_see_in_calendar: bool = True
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None

View File

@@ -442,6 +442,126 @@ def send_employee_removal_email(
) )
def send_company_approval_request_email(
to_email: str,
post_title: str,
post_content: str,
company_name: str,
approve_url: str,
reject_url: str,
image_url: Optional[str] = None
) -> bool:
"""Send email to employee asking them to approve or reject a company-reviewed post."""
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }}
.container {{ max-width: 600px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
.header {{ background: linear-gradient(135deg, #2d3838 0%, #1a2424 100%); color: white; padding: 24px; }}
.header h1 {{ margin: 0; font-size: 20px; }}
.content {{ padding: 24px; }}
.company-badge {{ display: inline-block; padding: 8px 16px; border-radius: 20px; background: #ffc700; color: #1a2424; font-weight: 600; margin: 8px 0; }}
.post-preview {{ background: #f8f9fa; border-left: 4px solid #ffc700; padding: 16px; margin: 20px 0; border-radius: 0 8px 8px 0; white-space: pre-wrap; font-size: 14px; line-height: 1.6; color: #333; }}
.buttons {{ display: flex; gap: 12px; margin-top: 24px; }}
.btn {{ display: inline-block; padding: 14px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 14px; text-align: center; }}
.btn-approve {{ background: #22c55e; color: white; }}
.btn-reject {{ background: #ef4444; color: white; }}
.footer {{ padding: 16px 24px; background: #f8f9fa; font-size: 12px; color: #666; text-align: center; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Dein Post wartet auf deine Freigabe</h1>
</div>
<div class="content">
<p>Hallo,</p>
<p>Das Unternehmen <span class="company-badge">{company_name}</span> hat deinen Post bearbeitet und bittet um deine Freigabe:</p>
<p><strong>{post_title}</strong></p>
<div class="post-preview">{post_content}</div>
{f'<img src="{image_url}" alt="Post-Bild" style="width: 100%; max-height: 400px; object-fit: cover; border-radius: 8px; margin: 16px 0;" />' if image_url else ''}
<p>Bitte entscheide, ob der Post so veröffentlicht werden soll:</p>
<div class="buttons">
<a href="{approve_url}" class="btn btn-approve">Freigeben</a>
<a href="{reject_url}" class="btn btn-reject">Ablehnen</a>
</div>
</div>
<div class="footer">
Diese Email wurde automatisch generiert. Die Links sind 72 Stunden gültig.
</div>
</div>
</body>
</html>
"""
return send_email(
to_email=to_email,
subject=f"Freigabe erforderlich: {post_title}",
html_content=html_content
)
def send_company_review_notification_email(
to_email: str,
post_id: UUID,
post_title: str,
company_name: str,
base_url: str
) -> bool:
"""Send email to employee notifying them the company has reviewed their post."""
post_url = f"{base_url}/posts/{post_id}"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }}
.container {{ max-width: 600px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
.header {{ background: linear-gradient(135deg, #2d3838 0%, #1a2424 100%); color: white; padding: 24px; }}
.header h1 {{ margin: 0; font-size: 20px; }}
.content {{ padding: 24px; }}
.company-badge {{ display: inline-block; padding: 8px 16px; border-radius: 20px; background: #ffc700; color: #1a2424; font-weight: 600; margin: 16px 0; }}
.btn {{ display: inline-block; padding: 14px 28px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 14px; background: #ffc700; color: #1a2424; margin-top: 16px; }}
.footer {{ padding: 16px 24px; background: #f8f9fa; font-size: 12px; color: #666; text-align: center; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Dein Post wurde bearbeitet</h1>
</div>
<div class="content">
<p>Hallo,</p>
<p>Das Unternehmen <span class="company-badge">{company_name}</span> hat deinen Post in Bearbeitung genommen:</p>
<p><strong>{post_title}</strong></p>
<p>Der Post wurde in die Spalte "Bearbeitet" verschoben. Du kannst ihn in deinem Dashboard einsehen.</p>
<a href="{post_url}" class="btn">Post ansehen</a>
</div>
<div class="footer">
Diese Email wurde automatisch generiert.
</div>
</div>
</body>
</html>
"""
return send_email(
to_email=to_email,
subject=f"Dein Post wurde bearbeitet: {post_title}",
html_content=html_content
)
def send_decision_notification_email( def send_decision_notification_email(
to_email: str, to_email: str,
post_title: str, post_title: str,

View File

@@ -39,6 +39,13 @@
main.sidebar-collapsed { margin-left: 4rem; } main.sidebar-collapsed { margin-left: 4rem; }
.toggle-btn { cursor: pointer; transition: transform 0.3s ease; } .toggle-btn { cursor: pointer; transition: transform 0.3s ease; }
aside.collapsed .toggle-btn { transform: rotate(180deg); } aside.collapsed .toggle-btn { transform: rotate(180deg); }
/* Hide nav scrollbar when sidebar collapsed */
aside.collapsed nav { overflow-y: hidden; overflow-x: hidden; }
/* Subtle scrollbar inside nav */
nav::-webkit-scrollbar { width: 4px; }
nav::-webkit-scrollbar-track { background: transparent; }
nav::-webkit-scrollbar-thumb { background: #4a5858; border-radius: 4px; }
nav::-webkit-scrollbar-thumb:hover { background: #5a6868; }
</style> </style>
<!-- Prevent sidebar flash on page load --> <!-- Prevent sidebar flash on page load -->
@@ -110,7 +117,7 @@
</div> </div>
{% endif %} {% endif %}
<nav class="flex-1 p-4 space-y-2"> <nav class="flex-1 p-4 space-y-2 overflow-y-auto min-h-0">
<a href="/" 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 %}"> <a href="/" 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 flex-shrink-0" 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> <svg class="w-5 h-5 flex-shrink-0" 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>
<span class="sidebar-text">Dashboard</span> <span class="sidebar-text">Dashboard</span>
@@ -142,6 +149,10 @@
<svg class="w-5 h-5 flex-shrink-0" 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> <svg class="w-5 h-5 flex-shrink-0" 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>
<span class="sidebar-text">Status</span> <span class="sidebar-text">Status</span>
</a> </a>
<a href="/calendar" 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 == 'calendar' %}active{% endif %}">
<svg class="w-5 h-5 flex-shrink-0" 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>
<span class="sidebar-text">Mein Kalender</span>
</a>
{% if session and session.account_type == 'company' %} {% if session and session.account_type == 'company' %}
<a href="/company/accounts" 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 == 'accounts' %}active{% endif %}"> <a href="/company/accounts" 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 == 'accounts' %}active{% endif %}">
<svg class="w-5 h-5 flex-shrink-0" 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> <svg class="w-5 h-5 flex-shrink-0" 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>

View File

@@ -39,6 +39,13 @@
main.sidebar-collapsed { margin-left: 4rem; } main.sidebar-collapsed { margin-left: 4rem; }
.toggle-btn { cursor: pointer; transition: transform 0.3s ease; } .toggle-btn { cursor: pointer; transition: transform 0.3s ease; }
aside.collapsed .toggle-btn { transform: rotate(180deg); } aside.collapsed .toggle-btn { transform: rotate(180deg); }
/* Hide nav scrollbar when sidebar collapsed */
aside.collapsed nav { overflow-y: hidden; overflow-x: hidden; }
/* Subtle scrollbar inside nav */
nav::-webkit-scrollbar { width: 4px; }
nav::-webkit-scrollbar-track { background: transparent; }
nav::-webkit-scrollbar-thumb { background: #4a5858; border-radius: 4px; }
nav::-webkit-scrollbar-thumb:hover { background: #5a6868; }
</style> </style>
<!-- Prevent sidebar flash on page load --> <!-- Prevent sidebar flash on page load -->
@@ -106,7 +113,7 @@
</div> </div>
{% endif %} {% endif %}
<nav class="flex-1 p-4 space-y-2"> <nav class="flex-1 p-4 space-y-2 overflow-y-auto min-h-0">
<a href="/" 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 %}"> <a href="/" 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 flex-shrink-0" 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> <svg class="w-5 h-5 flex-shrink-0" 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>
<span class="sidebar-text">Dashboard</span> <span class="sidebar-text">Dashboard</span>

View File

@@ -338,10 +338,16 @@
</div> </div>
<p class="text-white text-sm font-medium mb-1">{{ post.topic_title }}</p> <p class="text-white text-sm font-medium mb-1">{{ post.topic_title }}</p>
<p class="text-gray-400 text-xs line-clamp-2">{{ post.post_content[:100] }}...</p> <p class="text-gray-400 text-xs line-clamp-2">{{ post.post_content[:100] }}...</p>
{% if post.can_schedule %}
<button onclick="openScheduleModalForPost('{{ post.id }}', '{{ post.topic_title|e }}')" <button onclick="openScheduleModalForPost('{{ post.id }}', '{{ post.topic_title|e }}')"
class="mt-3 w-full py-2 text-xs bg-brand-highlight text-brand-bg-dark rounded hover:bg-brand-highlight-dark transition-colors"> class="mt-3 w-full py-2 text-xs bg-brand-highlight text-brand-bg-dark rounded hover:bg-brand-highlight-dark transition-colors">
Einplanen Einplanen
</button> </button>
{% else %}
<div class="mt-3 w-full py-2 text-xs text-center text-gray-500 bg-gray-700/30 rounded">
Keine Berechtigung zum Einplanen
</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -362,7 +368,9 @@
<select id="schedulePostSelect" name="post_id_select" class="w-full input-bg border rounded-lg px-4 py-2 text-white"> <select id="schedulePostSelect" name="post_id_select" class="w-full input-bg border rounded-lg px-4 py-2 text-white">
<option value="">-- Post auswahlen --</option> <option value="">-- Post auswahlen --</option>
{% for post in unscheduled_posts %} {% for post in unscheduled_posts %}
{% if post.can_schedule %}
<option value="{{ post.id }}">{{ post.employee_name }}: {{ post.topic_title[:40] }}...</option> <option value="{{ post.id }}">{{ post.employee_name }}: {{ post.topic_title[:40] }}...</option>
{% endif %}
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View File

@@ -57,14 +57,85 @@
</div> </div>
</div> </div>
<!-- Quick Actions (always shown, each gated by its own permission) -->
<div class="card-bg border rounded-xl p-6 mb-8">
<h3 class="text-lg font-semibold text-white mb-4">Aktionen</h3>
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-4">
{% if not selected_permissions or selected_permissions.can_do_research %}
<a href="/company/manage/research?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center flex-shrink-0">
<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>
<div>
<p class="text-white font-medium">Recherche</p>
<p class="text-gray-400 text-sm">Themen recherchieren</p>
</div>
</a>
{% endif %}
{% if not selected_permissions or selected_permissions.can_create_posts %}
<a href="/company/manage/create?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center flex-shrink-0">
<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="M12 4v16m8-8H4"/></svg>
</div>
<div>
<p class="text-white font-medium">Neuer Post</p>
<p class="text-gray-400 text-sm">KI-generiert erstellen</p>
</div>
</a>
<a href="/company/manage/chat-create?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center flex-shrink-0">
<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="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
</div>
<div>
<p class="text-white font-medium">Chat erstellen</p>
<p class="text-gray-400 text-sm">Im Dialog erstellen</p>
</div>
</a>
{% endif %}
{% if not selected_permissions or selected_permissions.can_manage_post_types %}
<a href="/company/manage/post-types?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center flex-shrink-0">
<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="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>
</div>
<div>
<p class="text-white font-medium">Post-Typen</p>
<p class="text-gray-400 text-sm">Kategorien verwalten</p>
</div>
</a>
{% endif %}
{% if not selected_permissions or selected_permissions.can_view_posts %}
<a href="/company/manage/posts?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center flex-shrink-0">
<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="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="text-white font-medium">Alle Posts</p>
<p class="text-gray-400 text-sm">Posts anzeigen</p>
</div>
</a>
{% endif %}
</div>
</div>
<!-- Stats + Recent Posts (only if can_view_posts) -->
{% if selected_permissions and not selected_permissions.can_view_posts %}
<div class="card-bg border border-red-500/30 rounded-xl p-6 mb-8 flex items-center gap-4">
<div class="w-12 h-12 bg-red-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
</div>
<div>
<p class="text-white font-medium">Posts nicht zugänglich</p>
<p class="text-gray-400 text-sm">{{ selected_employee.display_name or selected_employee.email }} hat den Zugriff auf Posts deaktiviert.</p>
</div>
</div>
{% else %}
<!-- Stats --> <!-- Stats -->
<div class="grid md:grid-cols-3 gap-6 mb-8"> <div class="grid md:grid-cols-3 gap-6 mb-8">
<div class="card-bg border rounded-xl p-6"> <div class="card-bg border rounded-xl p-6">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center"> <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"> <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="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>
<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>
<div> <div>
<p class="text-3xl font-bold text-white">{{ employee_posts | length }}</p> <p class="text-3xl font-bold text-white">{{ employee_posts | length }}</p>
@@ -72,13 +143,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-bg border rounded-xl p-6"> <div class="card-bg border rounded-xl p-6">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="w-12 h-12 bg-yellow-500/20 rounded-lg flex items-center justify-center"> <div class="w-12 h-12 bg-yellow-500/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-yellow-400" 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>
<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>
</div> </div>
<div> <div>
<p class="text-3xl font-bold text-white">{{ pending_posts }}</p> <p class="text-3xl font-bold text-white">{{ pending_posts }}</p>
@@ -86,13 +154,10 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-bg border rounded-xl p-6"> <div class="card-bg border rounded-xl p-6">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center"> <div class="w-12 h-12 bg-green-500/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"> <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 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div> </div>
<div> <div>
<p class="text-3xl font-bold text-white">{{ approved_posts }}</p> <p class="text-3xl font-bold text-white">{{ approved_posts }}</p>
@@ -102,46 +167,6 @@
</div> </div>
</div> </div>
<!-- Quick Actions -->
<div class="card-bg border rounded-xl p-6 mb-8">
<h3 class="text-lg font-semibold text-white mb-4">Aktionen</h3>
<div class="grid md:grid-cols-3 gap-4">
<a href="/company/manage/research?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<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>
<div>
<p class="text-white font-medium">Research Topics</p>
<p class="text-gray-400 text-sm">Themen recherchieren</p>
</div>
</a>
<a href="/company/manage/create?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<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="M12 4v16m8-8H4"/>
</svg>
</div>
<div>
<p class="text-white font-medium">Neuer Post</p>
<p class="text-gray-400 text-sm">KI-generiert erstellen</p>
</div>
</a>
<a href="/company/manage/posts?employee_id={{ selected_employee.id }}" 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 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
<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="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="text-white font-medium">Alle Posts</p>
<p class="text-gray-400 text-sm">Posts anzeigen</p>
</div>
</a>
</div>
</div>
<!-- Recent Posts --> <!-- Recent Posts -->
{% if employee_posts %} {% if employee_posts %}
<div class="card-bg border rounded-xl p-6"> <div class="card-bg border rounded-xl p-6">
@@ -168,15 +193,15 @@
{% else %} {% else %}
<div class="card-bg border rounded-xl p-6 text-center"> <div class="card-bg border rounded-xl p-6 text-center">
<div class="w-16 h-16 bg-gray-600/30 rounded-full flex items-center justify-center mx-auto mb-4"> <div class="w-16 h-16 bg-gray-600/30 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-8 h-8 text-gray-500" 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>
<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">Noch keine Posts vorhanden</p> <p class="text-gray-400">Noch keine Posts vorhanden</p>
<a href="/company/manage/create?employee_id={{ selected_employee.id }}" class="inline-block mt-4 text-brand-highlight hover:underline">Ersten Post erstellen</a> <a href="/company/manage/create?employee_id={{ selected_employee.id }}" class="inline-block mt-4 text-brand-highlight hover:underline">Ersten Post erstellen</a>
</div> </div>
{% endif %} {% endif %}
{% endif %}<!-- end can_view_posts check -->
{% else %} {% else %}
<!-- No Employee Selected --> <!-- No Employee Selected -->
{% if active_employees %} {% if active_employees %}

View File

@@ -0,0 +1,229 @@
{% extends "company_base.html" %}
{% block title %}Chat Assistent - {{ employee_name }}{% endblock %}
{% block head %}
<style>
body { background-color: #3d4848; }
#mainContent > div { padding: 0 !important; }
.chat-fixed-header, .chat-fixed-input { left: 256px; right: 0; transition: left 0.3s ease; }
aside.collapsed ~ main .chat-fixed-header,
aside.collapsed ~ main .chat-fixed-input { left: 64px; }
.dot { animation: bounce 1.4s infinite ease-in-out; display: inline-block; }
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-8px); } }
.post-type-chip { transition: all 0.2s; }
</style>
{% endblock %}
{% block content %}
<!-- Header -->
<div class="chat-fixed-header fixed bg-brand-bg z-20 top-0">
<div class="px-8 py-4 flex items-center gap-4">
<a href="/company/manage/posts?employee_id={{ employee_id }}" class="text-gray-400 hover:text-white 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="M15 19l-7-7 7-7"/></svg>
</a>
<div>
<h1 class="text-xl font-bold text-white">💬 Chat Assistent</h1>
<p class="text-xs text-gray-400">Für {{ employee_name }}</p>
</div>
</div>
</div>
<!-- Messages Area -->
<div id="chat-messages" class="px-8 space-y-4 pb-64 max-w-[80%] mx-auto" style="padding-top: 90px;">
<div class="flex items-start gap-3">
<div class="w-8 h-8 bg-brand-highlight/20 rounded-full flex items-center justify-center flex-shrink-0">
<span class="text-lg">🤖</span>
</div>
<div class="max-w-[70%]">
<div class="bg-brand-bg-dark rounded-2xl p-4 border border-gray-600">
<p class="text-gray-300">
Hallo! Ich helfe dir, einen LinkedIn-Post für <strong>{{ employee_name }}</strong> zu erstellen.
Beschreibe, worüber der Post handeln soll.
</p>
</div>
</div>
</div>
</div>
<!-- Input Area -->
<div class="chat-fixed-input fixed bottom-0 bg-brand-bg z-10">
<div class="pt-4 pb-6 px-8">
<div class="space-y-3 mx-auto" style="max-width: 768px;">
<!-- Post Type Chips -->
{% if post_types %}
<div class="flex flex-wrap gap-1.5" id="post-type-chips">
{% for pt in post_types %}
<button onclick="selectPostType('{{ pt.id }}', this)"
data-post-type-id="{{ pt.id }}"
class="post-type-chip px-3 py-1 rounded-full text-xs font-medium
{% if loop.first %}bg-brand-highlight text-black{% else %}bg-brand-bg-dark text-gray-400 hover:bg-brand-bg-light hover:text-gray-300{% endif %}"
{% if loop.first %}data-selected="true"{% endif %}>
{{ pt.name }}
</button>
{% endfor %}
</div>
{% else %}
<p class="text-yellow-400 text-sm">Keine Post-Typen für diesen Mitarbeiter vorhanden. Bitte erst Post-Typen anlegen.</p>
{% endif %}
<!-- Input Bar -->
<div class="flex gap-3 items-center">
<div class="flex-1 relative">
<input type="text" id="chat-input"
placeholder="{% if limit_reached %}Token-Limit erreicht morgen wieder verfügbar{% else %}Beschreibe den Post...{% endif %}"
class="w-full input-bg border border-gray-600 rounded-full px-6 py-3 text-white focus:outline-none focus:border-brand-highlight transition-colors {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}"
onkeydown="handleChatKeydown(event)"
{% if limit_reached %}disabled{% endif %}>
</div>
<button onclick="sendMessage()" id="send-btn"
class="w-12 h-12 bg-brand-highlight hover:bg-yellow-500 text-black rounded-full transition-all flex items-center justify-center flex-shrink-0 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}"
{% if limit_reached %}disabled{% 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="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
</button>
<button onclick="savePost()" id="save-btn"
class="hidden w-12 h-12 bg-green-600 hover:bg-green-500 text-white rounded-full transition-all items-center justify-center flex-shrink-0"
title="Post speichern">
<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="M5 13l4 4L19 7"/></svg>
</button>
</div>
</div>
</div>
</div>
<script>
let chatHistory = [];
let currentPost = null;
let selectedPostTypeId = null;
const EMPLOYEE_ID = "{{ employee_id }}";
let tokenLimitReached = {{ 'true' if limit_reached else 'false' }};
document.addEventListener('DOMContentLoaded', () => {
const firstChip = document.querySelector('.post-type-chip[data-selected="true"]');
if (firstChip) selectedPostTypeId = firstChip.dataset.postTypeId;
});
function selectPostType(postTypeId, element) {
selectedPostTypeId = postTypeId;
document.querySelectorAll('.post-type-chip').forEach(chip => {
chip.classList.remove('bg-brand-highlight', 'text-black');
chip.classList.add('bg-brand-bg-dark', 'text-gray-400');
chip.removeAttribute('data-selected');
});
element.classList.remove('bg-brand-bg-dark', 'text-gray-400');
element.classList.add('bg-brand-highlight', 'text-black');
element.setAttribute('data-selected', 'true');
}
function handleChatKeydown(event) {
if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }
}
async function sendMessage() {
if (tokenLimitReached) return;
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) { showToast('Bitte gib eine Nachricht ein', 'error'); return; }
if (!selectedPostTypeId) { showToast('Bitte wähle einen Post-Typ aus', 'error'); return; }
addMessageToChat('user', message);
input.value = '';
const container = document.getElementById('chat-messages');
const tempMsg = document.createElement('div');
tempMsg.id = 'temp-msg';
tempMsg.className = 'flex items-start gap-3';
tempMsg.innerHTML = `<div class="w-8 h-8 bg-brand-highlight/20 rounded-full flex items-center justify-center flex-shrink-0"><span class="text-lg">🤖</span></div><div class="max-w-[70%]"><div class="bg-brand-bg-dark rounded-2xl p-4 border border-gray-600"><p class="text-gray-300">wird generiert<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span></p></div></div>`;
container.appendChild(tempMsg);
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
const sendBtn = document.getElementById('send-btn');
const originalHTML = sendBtn.innerHTML;
sendBtn.disabled = true;
sendBtn.innerHTML = '<svg class="w-5 h-5 animate-spin" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" opacity="0.3"/><path d="M12 2v4c3.31 0 6 2.69 6 6h4c0-5.52-4.48-10-10-10z"/></svg>';
try {
const endpoint = currentPost ? null : '/api/company/manage/chat/generate';
// For refinement, use generate with current_post context
const response = await fetch('/api/company/manage/chat/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employee_id: EMPLOYEE_ID, message: message, post_type_id: selectedPostTypeId, current_post: currentPost, chat_history: chatHistory })
});
const result = await response.json();
document.getElementById('temp-msg')?.remove();
if (result.success) {
currentPost = result.post;
addMessageToChat('ai', result.explanation || 'Hier ist der Entwurf:', result.post);
chatHistory.push({ user: message, ai: result.post, explanation: result.explanation });
const saveBtn = document.getElementById('save-btn');
saveBtn.classList.remove('hidden');
saveBtn.classList.add('flex');
} else if (result.token_limit_exceeded) {
tokenLimitReached = true;
showToast(result.error || 'Token-Limit erreicht', 'error');
} else {
showToast('Fehler: ' + (result.error || 'Unbekannter Fehler'), 'error');
addMessageToChat('ai', '❌ Fehler beim Generieren. Bitte versuche es erneut.');
}
} catch (error) {
document.getElementById('temp-msg')?.remove();
showToast('Netzwerkfehler', 'error');
} finally {
sendBtn.disabled = false;
sendBtn.innerHTML = originalHTML;
}
}
function addMessageToChat(type, text, post = null) {
const container = document.getElementById('chat-messages');
const div = document.createElement('div');
const postHtml = post ? `<div class="mt-3 p-4 bg-brand-bg rounded-2xl border border-brand-highlight/30"><span class="text-brand-highlight font-medium text-sm">Post-Entwurf</span><div class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed mt-2">${escapeHtml(post)}</div></div>` : '';
if (type === 'user') {
div.className = 'flex items-start gap-3 justify-end';
div.innerHTML = `<div class="max-w-[70%]"><div class="bg-blue-900/30 rounded-2xl p-4 border border-blue-700/50"><p class="text-white">${escapeHtml(text)}</p></div></div><div class="w-8 h-8 bg-brand-highlight rounded-full flex items-center justify-center flex-shrink-0"><span class="text-brand-bg-dark font-bold text-sm">K</span></div>`;
} else {
div.className = 'flex items-start gap-3';
div.innerHTML = `<div class="w-8 h-8 bg-brand-highlight/20 rounded-full flex items-center justify-center flex-shrink-0"><span class="text-lg">🤖</span></div><div class="max-w-[70%]"><div class="bg-brand-bg-dark rounded-2xl p-4 border border-gray-600"><p class="text-gray-300">${escapeHtml(text)}</p>${postHtml}</div></div>`;
}
container.appendChild(div);
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
}
async function savePost() {
if (!currentPost) { showToast('Kein Post zum Speichern', 'error'); return; }
const saveBtn = document.getElementById('save-btn');
const originalHTML = saveBtn.innerHTML;
saveBtn.disabled = true;
saveBtn.innerHTML = '<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>';
try {
const topicTitle = (chatHistory[0]?.user || currentPost).slice(0, 80);
const response = await fetch('/api/company/manage/chat/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employee_id: EMPLOYEE_ID, post_content: currentPost, post_type_id: selectedPostTypeId, chat_history: chatHistory, topic_title: topicTitle })
});
const result = await response.json();
if (result.success) {
showToast('Post gespeichert!', 'success');
setTimeout(() => { window.location.href = `/company/manage/post/${result.post_id}?employee_id=${EMPLOYEE_ID}`; }, 1000);
} else {
showToast('Fehler: ' + (result.error || 'Speichern fehlgeschlagen'), 'error');
}
} catch (e) {
showToast('Netzwerkfehler beim Speichern', 'error');
} finally {
saveBtn.disabled = false;
saveBtn.innerHTML = originalHTML;
}
}
function escapeHtml(text) {
if (!text) return '';
return text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#039;');
}
</script>
{% endblock %}

View File

@@ -138,13 +138,46 @@
<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 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> <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 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
LinkedIn Post LinkedIn Post
</h2> </h2>
<button onclick="copyToClipboard()" class="px-3 py-1.5 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-gray-300 transition-colors flex items-center gap-2"> <div class="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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg> {% if not permissions or permissions.can_edit_posts %}
Kopieren <button onclick="toggleEditView()" id="editToggleBtn" class="px-3 py-1.5 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-gray-300 transition-colors flex items-center gap-2">
</button> <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="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>
Bearbeiten
</button>
{% endif %}
<button onclick="copyToClipboard()" class="px-3 py-1.5 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-gray-300 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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
Kopieren
</button>
</div>
</div> </div>
<!-- Edit View (hidden by default) -->
{% if not permissions or permissions.can_edit_posts %}
<div id="editView" class="hidden mb-4">
<textarea id="editTextarea"
class="w-full bg-brand-bg border border-gray-600 rounded-xl p-4 text-gray-200 text-sm leading-relaxed resize-none focus:outline-none focus:border-brand-highlight transition-colors"
rows="12"
style="min-height: 200px; font-family: inherit;">{{ post.post_content }}</textarea>
<div class="flex items-center justify-between mt-3">
<span class="text-sm text-gray-500" id="charCountLabel">
<span id="charCount">{{ post.post_content | length }}</span> / 3000 Zeichen
</span>
<div class="flex items-center gap-2">
<button onclick="cancelEdit()" class="px-4 py-2 bg-brand-bg hover:bg-brand-bg-light text-gray-300 rounded-lg text-sm transition-colors">
Abbrechen
</button>
<button onclick="saveEdit()" id="saveEditBtn" class="px-4 py-2 bg-brand-highlight hover:bg-yellow-500 text-brand-bg-dark font-medium rounded-lg text-sm 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="M5 13l4 4L19 7"/></svg>
Speichern
</button>
</div>
</div>
</div>
{% endif %}
<!-- LinkedIn Preview --> <!-- LinkedIn Preview -->
<div id="previewSection">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2 text-sm text-gray-400"> <div class="flex items-center gap-2 text-sm text-gray-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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
@@ -252,6 +285,7 @@
</div> </div>
</div> </div>
</div> </div>
</div><!-- end previewSection -->
</div> </div>
</div> </div>
@@ -276,6 +310,94 @@
</div> </div>
</div> </div>
<!-- AI Suggestions Panel -->
{% if not permissions or permissions.can_edit_posts %}
<div class="section-card rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold text-white 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="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
KI-Verbesserungen
</h3>
<button onclick="loadSuggestions()" id="refreshSuggestionsBtn" {% if limit_reached %}disabled{% endif %} class="p-1.5 rounded-lg bg-brand-bg hover:bg-brand-bg-light text-gray-400 hover:text-white transition-colors {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}" title="{% if limit_reached %}{{ limit_message }}{% else %}KI-Vorschläge generieren{% endif %}">
<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>
</button>
</div>
<!-- Quick Suggestions -->
<div id="quickSuggestions" class="space-y-2 mb-4">
<p class="text-xs text-gray-500 mb-3">Schnelle Anpassungen:</p>
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Mache den Hook emotionaler und aufmerksamkeitsstärker')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight"></span> Hook verstärken
</button>
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Füge einen starken Call-to-Action am Ende hinzu')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">🎯</span> Call-to-Action hinzufügen
</button>
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Füge eine kurze persönliche Anekdote oder Erfahrung hinzu')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">📖</span> Storytelling einbauen
</button>
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Verbessere die Struktur: Kürzere Absätze, mehr Weißraum, bessere Lesbarkeit')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">📝</span> Struktur optimieren
</button>
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Kürze den Post auf das Wesentliche, entferne überflüssige Worte')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">✂️</span> Kürzer & prägnanter
</button>
</div>
<!-- Loading State -->
<div id="suggestionsLoading" class="hidden flex items-center justify-center py-4">
<div class="loading-spinner"></div>
<span class="ml-2 text-sm text-gray-400">Generiere Vorschläge...</span>
</div>
<!-- AI Generated Suggestions List -->
<div id="suggestionsList" class="space-y-3 hidden"></div>
<!-- Custom Instruction -->
<div class="mt-4 pt-4 border-t border-brand-bg-light">
<label class="text-xs text-gray-400 block mb-2">Eigene Anweisung</label>
<div class="flex gap-2">
<input type="text" id="customSuggestion" placeholder="z.B. Mache es humorvoller" class="flex-1 px-3 py-2 bg-brand-bg border border-brand-bg-light rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-highlight {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}" {% if limit_reached %}disabled{% endif %}>
<button {% if not limit_reached %}onclick="applyCustomSuggestion()"{% endif %} id="applyCustomBtn" class="px-3 py-2 bg-brand-highlight hover:bg-brand-highlight/90 text-brand-bg-dark rounded-lg transition-colors {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}" {% if limit_reached %}disabled{% endif %}>
<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="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>
</button>
</div>
</div>
</div>
{% endif %}
<!-- Schedule Section -->
{% if not permissions or permissions.can_schedule_posts %}
<div class="section-card rounded-xl p-6">
<h3 class="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="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>
Planen
</h3>
{% if post.status == 'scheduled' and post.scheduled_at %}
<div class="mb-3 p-3 bg-brand-highlight/10 border border-brand-highlight/30 rounded-lg">
<p class="text-xs text-gray-400 mb-1">Geplant für</p>
<p class="text-white font-medium text-sm">{{ post.scheduled_at.strftime('%d.%m.%Y %H:%M') }} Uhr</p>
</div>
<button onclick="unschedulePost()" class="w-full px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 text-red-300 rounded-lg text-sm transition-colors flex items-center justify-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="M6 18L18 6M6 6l12 12"/></svg>
Planung aufheben
</button>
{% else %}
{% if post.status != 'ready' %}
<p class="text-xs text-gray-500 mb-3">Nur freigegebene Posts (Status "Freigegeben") können geplant werden.</p>
{% endif %}
<input type="datetime-local" id="scheduleInput"
class="w-full bg-brand-bg border border-gray-600 rounded-lg px-3 py-2 text-white text-sm mb-3 focus:outline-none focus:border-brand-highlight transition-colors {% if post.status != 'ready' %}opacity-50 cursor-not-allowed{% endif %}"
{% if post.status != 'ready' %}disabled{% endif %}>
<button onclick="schedulePost()" id="scheduleBtn"
class="w-full px-4 py-2.5 bg-brand-highlight hover:bg-yellow-500 text-brand-bg-dark font-medium rounded-lg text-sm transition-colors flex items-center justify-center gap-2 {% if post.status != 'ready' %}opacity-50 cursor-not-allowed{% endif %}"
{% if post.status != 'ready' %}disabled{% endif %}>
<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="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 planen
</button>
{% endif %}
</div>
{% endif %}
<!-- Media Upload Section (Multi-Media Support) --> <!-- Media Upload Section (Multi-Media Support) -->
<div class="section-card rounded-xl p-6"> <div class="section-card rounded-xl p-6">
<h3 class="font-semibold text-white mb-4 flex items-center gap-2"> <h3 class="font-semibold text-white mb-4 flex items-center gap-2">
@@ -287,19 +409,22 @@
<div id="mediaGrid" class="grid gap-3 mb-3 {% if not post.media_items or post.media_items | length == 0 %}hidden{% endif %}" style="grid-template-columns: repeat({{ post.media_items | length if post.media_items and post.media_items | length <= 3 else 1 }}, 1fr);"> <div id="mediaGrid" class="grid gap-3 mb-3 {% if not post.media_items or post.media_items | length == 0 %}hidden{% endif %}" style="grid-template-columns: repeat({{ post.media_items | length if post.media_items and post.media_items | length <= 3 else 1 }}, 1fr);">
{% if post.media_items %} {% if post.media_items %}
{% for item in post.media_items %} {% for item in post.media_items %}
<div class="media-item relative group rounded-lg overflow-hidden" data-index="{{ item.order if item.order is defined else loop.index0 }}" draggable="true" style="cursor: grab;"> <div class="media-item relative group rounded-lg overflow-hidden" data-index="{{ item.order if item.order is defined else loop.index0 }}"
{% if not permissions or permissions.can_edit_posts %}draggable="true" style="cursor: grab;"{% else %}style="cursor: default;"{% endif %}>
{% if item.type == 'image' %} {% if item.type == 'image' %}
<img src="{{ item.url }}" alt="Media {{ loop.index }}" class="w-full h-48 object-cover"> <img src="{{ item.url }}" alt="Media {{ loop.index }}" class="w-full h-48 object-cover">
{% elif item.type == 'video' %} {% elif item.type == 'video' %}
<video src="{{ item.url }}" class="w-full h-48 object-cover" controls></video> <video src="{{ item.url }}" class="w-full h-48 object-cover" controls></video>
{% endif %} {% endif %}
<!-- Delete button --> <!-- Delete button (only if can edit) -->
{% if not permissions or permissions.can_edit_posts %}
<button onclick="deleteMedia({{ item.order if item.order is defined else loop.index0 }})" class="absolute top-2 right-2 p-2 bg-red-600 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-700"> <button onclick="deleteMedia({{ item.order if item.order is defined else loop.index0 }})" class="absolute top-2 right-2 p-2 bg-red-600 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-700">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-white" 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"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg> </svg>
</button> </button>
{% endif %}
<!-- Order badge --> <!-- Order badge -->
<div class="absolute bottom-2 left-2 px-2 py-1 bg-black/50 rounded text-xs text-white"> <div class="absolute bottom-2 left-2 px-2 py-1 bg-black/50 rounded text-xs text-white">
@@ -310,7 +435,8 @@
{% endif %} {% endif %}
</div> </div>
<!-- Upload Zone (shown when < 3 media items) --> <!-- Upload Zone (only if can edit, shown when < 3 media items) -->
{% if not permissions or permissions.can_edit_posts %}
<div id="mediaUploadZone" class="{% if post.media_items and post.media_items | length >= 3 %}hidden{% endif %} border-2 border-dashed border-brand-bg-light rounded-xl p-6 text-center cursor-pointer hover:border-brand-highlight transition-colors"> <div id="mediaUploadZone" class="{% if post.media_items and post.media_items | length >= 3 %}hidden{% endif %} border-2 border-dashed border-brand-bg-light rounded-xl p-6 text-center cursor-pointer hover:border-brand-highlight transition-colors">
<input type="file" id="mediaFileInput" accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,video/quicktime" class="hidden"> <input type="file" id="mediaFileInput" accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,video/quicktime" class="hidden">
<svg class="w-8 h-8 mx-auto mb-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg> <svg class="w-8 h-8 mx-auto mb-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>
@@ -326,6 +452,11 @@
</div> </div>
<p class="text-xs text-gray-400 mt-1 text-center">Wird hochgeladen...</p> <p class="text-xs text-gray-400 mt-1 text-center">Wird hochgeladen...</p>
</div> </div>
{% else %}
{% if not post.media_items or post.media_items | length == 0 %}
<p class="text-xs text-gray-500 text-center py-4">Keine Medien vorhanden.</p>
{% endif %}
{% endif %}
</div> </div>
<!-- Post Info --> <!-- Post Info -->
@@ -342,8 +473,14 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-400">Zeichen</span> <span class="text-gray-400">Zeichen</span>
<span class="text-white">{{ post.post_content | length }}</span> <span class="text-white" id="charCountDisplay">{{ post.post_content | length }}</span>
</div> </div>
{% if post.scheduled_at %}
<div class="flex justify-between">
<span class="text-gray-400">Geplant</span>
<span class="text-brand-highlight text-xs">{{ post.scheduled_at.strftime('%d.%m.%Y %H:%M') }}</span>
</div>
{% endif %}
{% if post.topic_title %} {% if post.topic_title %}
<div class="pt-3 border-t border-brand-bg-light"> <div class="pt-3 border-t border-brand-bg-light">
<span class="text-gray-400 block mb-1">Topic</span> <span class="text-gray-400 block mb-1">Topic</span>
@@ -360,6 +497,23 @@
<script> <script>
const POST_ID = '{{ post.id }}'; const POST_ID = '{{ post.id }}';
const EMPLOYEE_ID = '{{ employee_id }}'; const EMPLOYEE_ID = '{{ employee_id }}';
const CAN_EDIT = {{ 'true' if (not permissions or permissions.can_edit_posts) else 'false' }};
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 text-sm ${
type === 'success' ? 'bg-green-600 text-white' :
type === 'error' ? 'bg-red-600 text-white' :
'bg-gray-700 text-white'
}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function copyToClipboard() { function copyToClipboard() {
const content = document.querySelector('.linkedin-content').textContent; const content = document.querySelector('.linkedin-content').textContent;
@@ -371,6 +525,128 @@ function copyToClipboard() {
}); });
} }
// ==================== EDIT FUNCTIONS ====================
function toggleEditView() {
const editView = document.getElementById('editView');
const previewSection = document.getElementById('previewSection');
const editBtn = document.getElementById('editToggleBtn');
if (editView.classList.contains('hidden')) {
editView.classList.remove('hidden');
previewSection.classList.add('hidden');
editBtn.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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> Vorschau';
updateCharCount();
} else {
cancelEdit();
}
}
function cancelEdit() {
const editView = document.getElementById('editView');
const previewSection = document.getElementById('previewSection');
const editBtn = document.getElementById('editToggleBtn');
if (editView) editView.classList.add('hidden');
if (previewSection) previewSection.classList.remove('hidden');
if (editBtn) editBtn.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="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> Bearbeiten';
}
function updateCharCount() {
const textarea = document.getElementById('editTextarea');
const charCount = document.getElementById('charCount');
const label = document.getElementById('charCountLabel');
if (!textarea || !charCount) return;
const len = textarea.value.length;
charCount.textContent = len;
if (label) {
label.style.color = len > 3000 ? '#ef4444' : len > 2700 ? '#f59e0b' : '#9ca3af';
}
}
async function saveEdit() {
const textarea = document.getElementById('editTextarea');
const newContent = textarea.value.trim();
const saveBtn = document.getElementById('saveEditBtn');
if (!newContent) { showToast('Der Post darf nicht leer sein.', 'error'); return; }
if (newContent.length > 3000) { showToast('Post überschreitet das LinkedIn-Limit von 3000 Zeichen.', 'error'); return; }
const originalHTML = saveBtn.innerHTML;
saveBtn.disabled = true;
saveBtn.innerHTML = '<div class="w-4 h-4 border-2 border-brand-bg-dark border-t-transparent rounded-full animate-spin"></div>';
try {
const formData = new FormData();
formData.append('content', newContent);
const response = await fetch(`/api/posts/${POST_ID}`, { method: 'PUT', body: formData });
const result = await response.json();
if (result.success) {
// Update LinkedIn preview content
const linkedinContent = document.querySelector('.linkedin-content');
if (linkedinContent) linkedinContent.textContent = newContent;
const charDisplay = document.getElementById('charCountDisplay');
if (charDisplay) charDisplay.textContent = newContent.length;
cancelEdit();
showToast('Post gespeichert!', 'success');
} else {
showToast('Fehler: ' + (result.detail || 'Speichern fehlgeschlagen'), 'error');
}
} catch (e) {
showToast('Netzwerkfehler', 'error');
} finally {
saveBtn.disabled = false;
saveBtn.innerHTML = originalHTML;
}
}
// ==================== SCHEDULE FUNCTIONS ====================
async function schedulePost() {
const input = document.getElementById('scheduleInput');
if (!input || !input.value) { showToast('Bitte Datum und Uhrzeit auswählen', 'error'); return; }
const scheduleBtn = document.getElementById('scheduleBtn');
const originalHTML = scheduleBtn.innerHTML;
scheduleBtn.disabled = true;
scheduleBtn.innerHTML = '<div class="w-4 h-4 border-2 border-brand-bg-dark border-t-transparent rounded-full animate-spin"></div>';
try {
const localDate = new Date(input.value);
const isoString = localDate.toISOString();
const formData = new FormData();
formData.append('scheduled_at', isoString);
const response = await fetch(`/api/posts/${POST_ID}/schedule`, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
showToast('Post geplant!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast('Fehler: ' + (result.detail || 'Planen fehlgeschlagen'), 'error');
}
} catch (e) {
showToast('Netzwerkfehler', 'error');
} finally {
scheduleBtn.disabled = false;
scheduleBtn.innerHTML = originalHTML;
}
}
async function unschedulePost() {
if (!confirm('Planung aufheben?')) return;
try {
const response = await fetch(`/api/posts/${POST_ID}/unschedule`, { method: 'POST' });
const result = await response.json();
if (result.success) {
showToast('Planung aufgehoben', 'success');
setTimeout(() => location.reload(), 800);
} else {
showToast('Fehler: ' + (result.detail || 'Aufheben fehlgeschlagen'), 'error');
}
} catch (e) {
showToast('Netzwerkfehler', 'error');
}
}
// Preview Mode Toggle (Desktop/Mobile) // Preview Mode Toggle (Desktop/Mobile)
function setPreviewMode(mode) { function setPreviewMode(mode) {
const preview = document.getElementById('linkedinPreview'); const preview = document.getElementById('linkedinPreview');
@@ -416,21 +692,178 @@ async function updateStatus(newStatus) {
} }
} }
// ==================== IMAGE UPLOAD ==================== // ==================== AI SUGGESTIONS ====================
function showToast(message, type = 'info') { async function loadSuggestions() {
const toast = document.createElement('div'); const loadingEl = document.getElementById('suggestionsLoading');
toast.className = `fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 transition-all transform translate-y-0 opacity-100 ${ const listEl = document.getElementById('suggestionsList');
type === 'success' ? 'bg-green-600 text-white' : const quickSuggestions = document.getElementById('quickSuggestions');
type === 'error' ? 'bg-red-600 text-white' : const refreshBtn = document.getElementById('refreshSuggestionsBtn');
'bg-brand-bg-light text-white' if (!loadingEl) return;
}`;
toast.textContent = message; loadingEl.classList.remove('hidden');
document.body.appendChild(toast); quickSuggestions.classList.add('hidden');
setTimeout(() => { listEl.classList.add('hidden');
toast.classList.add('opacity-0', 'translate-y-2'); listEl.innerHTML = '';
setTimeout(() => toast.remove(), 300); refreshBtn.disabled = true;
}, 3000);
try {
const response = await fetch(`/api/posts/${POST_ID}/suggestions`);
if (!response.ok) throw new Error('Fehler beim Laden der Vorschläge');
const result = await response.json();
const suggestions = result.suggestions || [];
loadingEl.classList.add('hidden');
if (suggestions.length === 0) {
listEl.innerHTML = '<p class="text-sm text-gray-400 text-center py-2">Keine Vorschläge verfügbar.</p>';
quickSuggestions.classList.remove('hidden');
} else {
listEl.innerHTML = '';
const header = document.createElement('p');
header.className = 'text-xs text-gray-500 mb-3';
header.textContent = 'Basierend auf Kritiker-Feedback:';
listEl.appendChild(header);
suggestions.forEach(s => {
const btn = document.createElement('button');
btn.className = 'w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2';
btn.innerHTML = `<span class="text-brand-highlight">✨</span> ${escapeHtml(s.label)}`;
btn.addEventListener('click', () => applySuggestion(s.action));
listEl.appendChild(btn);
});
const backBtn = document.createElement('button');
backBtn.className = 'w-full mt-3 px-3 py-2 text-sm text-gray-400 hover:text-white transition-colors';
backBtn.innerHTML = '← Zurück zu Schnelloptionen';
backBtn.addEventListener('click', () => {
listEl.classList.add('hidden');
quickSuggestions.classList.remove('hidden');
});
listEl.appendChild(backBtn);
listEl.classList.remove('hidden');
}
} catch (error) {
console.error('Error loading suggestions:', error);
loadingEl.classList.add('hidden');
quickSuggestions.classList.remove('hidden');
showToast('Fehler beim Laden der Vorschläge', 'error');
} finally {
refreshBtn.disabled = false;
}
}
async function applySuggestion(suggestion) {
const listEl = document.getElementById('suggestionsList');
const quickSuggestions = document.getElementById('quickSuggestions');
const loadingEl = document.getElementById('suggestionsLoading');
listEl.classList.add('hidden');
loadingEl.classList.remove('hidden');
try {
const formData = new FormData();
formData.append('suggestion', suggestion);
const response = await fetch(`/api/posts/${POST_ID}/revise`, { method: 'POST', body: formData });
if (!response.ok) throw new Error('Fehler beim Anwenden des Vorschlags');
const result = await response.json();
const newContent = result.new_content;
// Update the LinkedIn preview
const linkedinContent = document.querySelector('.linkedin-content');
if (linkedinContent) linkedinContent.textContent = newContent;
const editTextarea = document.getElementById('editTextarea');
if (editTextarea) editTextarea.value = newContent;
const charDisplay = document.getElementById('charCountDisplay');
if (charDisplay) charDisplay.textContent = newContent.length;
showToast('Vorschlag erfolgreich angewendet!', 'success');
loadingEl.classList.add('hidden');
quickSuggestions.classList.remove('hidden');
} catch (error) {
console.error('Error applying suggestion:', error);
showToast('Fehler: ' + error.message, 'error');
loadingEl.classList.add('hidden');
quickSuggestions.classList.remove('hidden');
}
}
async function applyQuickSuggestion(suggestion) {
const quickSuggestions = document.getElementById('quickSuggestions');
const loadingEl = document.getElementById('suggestionsLoading');
quickSuggestions.classList.add('hidden');
loadingEl.classList.remove('hidden');
try {
const formData = new FormData();
formData.append('suggestion', suggestion);
const response = await fetch(`/api/posts/${POST_ID}/revise`, { method: 'POST', body: formData });
if (!response.ok) throw new Error('Fehler beim Anwenden');
const result = await response.json();
const newContent = result.new_content;
const linkedinContent = document.querySelector('.linkedin-content');
if (linkedinContent) linkedinContent.textContent = newContent;
const editTextarea = document.getElementById('editTextarea');
if (editTextarea) editTextarea.value = newContent;
const charDisplay = document.getElementById('charCountDisplay');
if (charDisplay) charDisplay.textContent = newContent.length;
showToast('Verbesserung angewendet!', 'success');
} catch (error) {
console.error('Error applying quick suggestion:', error);
showToast('Fehler: ' + error.message, 'error');
} finally {
loadingEl.classList.add('hidden');
quickSuggestions.classList.remove('hidden');
}
}
async function applyCustomSuggestion() {
const input = document.getElementById('customSuggestion');
const suggestion = input.value.trim();
if (!suggestion) { showToast('Bitte gib eine Anweisung ein.', 'error'); return; }
const btn = document.getElementById('applyCustomBtn');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;"></div>';
btn.disabled = true;
try {
const formData = new FormData();
formData.append('suggestion', suggestion);
const response = await fetch(`/api/posts/${POST_ID}/revise`, { method: 'POST', body: formData });
if (!response.ok) throw new Error('Fehler beim Anwenden der Anweisung');
const result = await response.json();
const newContent = result.new_content;
const linkedinContent = document.querySelector('.linkedin-content');
if (linkedinContent) linkedinContent.textContent = newContent;
const editTextarea = document.getElementById('editTextarea');
if (editTextarea) editTextarea.value = newContent;
const charDisplay = document.getElementById('charCountDisplay');
if (charDisplay) charDisplay.textContent = newContent.length;
input.value = '';
showToast('Anweisung erfolgreich angewendet!', 'success');
} catch (error) {
console.error('Error applying custom suggestion:', error);
showToast('Fehler: ' + error.message, 'error');
} finally {
btn.innerHTML = originalHTML;
btn.disabled = false;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
} }
// ==================== MULTI-MEDIA UPLOAD ==================== // ==================== MULTI-MEDIA UPLOAD ====================
@@ -552,24 +985,26 @@ function refreshMediaGrid() {
const isDragged = draggedItemData && item.url === draggedItemData.url; const isDragged = draggedItemData && item.url === draggedItemData.url;
const ghostClass = isDragged ? 'is-ghost' : ''; const ghostClass = isDragged ? 'is-ghost' : '';
const ghostStyle = isDragged ? 'opacity: 0.3;' : ''; const ghostStyle = isDragged ? 'opacity: 0.3;' : '';
const deleteBtn = CAN_EDIT ? `
<button onclick="deleteMedia(${item.order})"
class="absolute top-2 right-2 p-2 bg-red-600 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-700 z-10">
<svg class="w-4 h-4 text-white" 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>` : '';
return ` return `
<div class="media-item relative group rounded-lg overflow-hidden border-2 border-transparent hover:border-brand-highlight ${ghostClass}" <div class="media-item relative group rounded-lg overflow-hidden border-2 border-transparent hover:border-brand-highlight ${ghostClass}"
data-array-index="${i}" data-array-index="${i}"
draggable="true" ${CAN_EDIT ? 'draggable="true"' : ''}
style="cursor: grab; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); ${ghostStyle}"> style="${CAN_EDIT ? 'cursor: grab;' : 'cursor: default;'} transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); ${ghostStyle}">
<div class="media-content"> <div class="media-content">
${item.type === 'image' ${item.type === 'image'
? `<img src="${item.url}" alt="Media ${i+1}" class="w-full h-48 object-cover pointer-events-none">` ? `<img src="${item.url}" alt="Media ${i+1}" class="w-full h-48 object-cover pointer-events-none">`
: `<video src="${item.url}" class="w-full h-48 object-cover pointer-events-none" controls></video>` : `<video src="${item.url}" class="w-full h-48 object-cover pointer-events-none" controls></video>`
} }
</div> </div>
<button onclick="deleteMedia(${item.order})" ${deleteBtn}
class="absolute top-2 right-2 p-2 bg-red-600 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-700 z-10">
<svg class="w-4 h-4 text-white" 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 class="absolute bottom-2 left-2 px-2 py-1 bg-black/50 rounded text-xs text-white pointer-events-none">${i+1}</div> <div class="absolute bottom-2 left-2 px-2 py-1 bg-black/50 rounded text-xs text-white pointer-events-none">${i+1}</div>
</div> </div>
`; `;
@@ -739,6 +1174,7 @@ async function saveReorderInBackground() {
} }
function initMediaUpload() { function initMediaUpload() {
if (!CAN_EDIT) return;
const uploadZone = document.getElementById('mediaUploadZone'); const uploadZone = document.getElementById('mediaUploadZone');
const fileInput = document.getElementById('mediaFileInput'); const fileInput = document.getElementById('mediaFileInput');
@@ -768,6 +1204,21 @@ function initMediaUpload() {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initMediaUpload(); initMediaUpload();
const editTextarea = document.getElementById('editTextarea');
if (editTextarea) {
editTextarea.addEventListener('input', updateCharCount);
}
// Pre-fill schedule input with current scheduled_at if exists
const scheduleInput = document.getElementById('scheduleInput');
{% if post.scheduled_at %}
if (scheduleInput) {
const dt = new Date('{{ post.scheduled_at.isoformat() }}');
const pad = n => String(n).padStart(2, '0');
scheduleInput.value = `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
}
{% endif %}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,694 @@
{% extends "company_base.html" %}
{% block title %}Post-Typen - {{ employee_name }} - {{ session.company_name }}{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto">
<!-- Breadcrumb -->
<div class="mb-6">
<nav class="flex items-center gap-2 text-sm">
<a href="/company/manage" class="text-gray-400 hover:text-white transition-colors">Inhalte verwalten</a>
<svg class="w-4 h-4 text-gray-600" 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>
<a href="/company/manage/posts?employee_id={{ employee_id }}" class="text-gray-400 hover:text-white transition-colors">{{ employee_name }}</a>
<svg class="w-4 h-4 text-gray-600" 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>
<span class="text-white">Post-Typen</span>
</nav>
</div>
<!-- Header -->
<div class="mb-6">
<div class="flex items-center gap-3 mb-4">
<a href="/company/manage/posts?employee_id={{ employee_id }}" onclick="return handleBackNavigation(event)" class="text-gray-400 hover:text-white transition-colors">
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
</a>
<div class="flex-1">
<h1 class="text-2xl font-bold text-white">Post-Typen von {{ employee_name }}</h1>
<p class="text-gray-400 mt-1">Definiere und konfiguriere die Post-Kategorien</p>
</div>
<button onclick="openCreateModal()" class="btn-primary px-5 py-2.5 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 4v16m8-8H4"/>
</svg>
Neuer Post-Typ
</button>
</div>
</div>
<!-- Empty State -->
{% if not post_types_with_counts %}
<div class="card-bg border rounded-xl p-12 text-center">
<div class="w-20 h-20 bg-brand-highlight/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-10 h-10 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-2">Noch keine Post-Typen definiert</h3>
<p class="text-gray-400 mb-6">Erstelle den ersten Post-Typ für {{ employee_name }}.</p>
<button onclick="openCreateModal()" class="btn-primary px-6 py-3 rounded-lg font-medium">
Ersten Post-Typ erstellen
</button>
</div>
{% endif %}
<!-- Post Types List -->
<div class="space-y-4" id="post-types-list"></div>
<!-- Initial data from server -->
<div id="initial-post-types" style="display:none;" data-json="{{ post_types_json | escape }}"></div>
</div>
<script>
const INITIAL_POST_TYPES = {{ post_types_json | safe }};
const EMPLOYEE_ID = "{{ employee_id }}";
// ========== MODAL FUNCTIONS ==========
function openCreateModal() {
document.getElementById('modal-title').textContent = 'Neuer Post-Typ';
document.getElementById('edit-id').value = '';
document.getElementById('edit-name').value = '';
document.getElementById('edit-description').value = '';
document.getElementById('edit-weight').value = '0.5';
updateModalWeightDisplay(0.5);
isEditMode = false;
currentEditId = null;
document.getElementById('modal-step-1').classList.remove('hidden');
document.getElementById('edit-form').classList.add('hidden');
document.getElementById('edit-modal').classList.remove('hidden');
document.getElementById('edit-modal').classList.add('flex');
}
function selectPredefinedType(typeName) {
const descriptions = {
'Tough Leadership': 'Meinungsbeiträge und Einblicke zu Branchenthemen, eigene Perspektiven und Expertenwissen',
'Company Updates': 'Neuigkeiten über das Unternehmen, Meilensteine, Produktankündigungen und Teamerfolge',
'Educational Content': 'Tutorials, How-Tos, Tipps & Tricks, Wissen teilen und Mehrwert bieten',
'Personal Story': 'Persönliche Erfahrungen, Learnings, Karrieremomente und authentische Geschichten',
'Industry News': 'Aktuelle Branchennews, Trends, Marktentwicklungen und relevante Ereignisse',
'Engagement Post': 'Umfragen, Fragen an die Community, Diskussionsanregungen und interaktive Inhalte'
};
document.getElementById('edit-name').value = typeName;
document.getElementById('edit-description').value = descriptions[typeName] || '';
document.getElementById('modal-step-1').classList.add('hidden');
document.getElementById('edit-form').classList.remove('hidden');
}
function goToCustomType() {
document.getElementById('edit-name').value = '';
document.getElementById('edit-description').value = '';
document.getElementById('modal-step-1').classList.add('hidden');
document.getElementById('edit-form').classList.remove('hidden');
}
function backToStep1() {
document.getElementById('modal-step-1').classList.remove('hidden');
document.getElementById('edit-form').classList.add('hidden');
}
function closeEditModal() {
document.getElementById('edit-modal').classList.add('hidden');
document.getElementById('edit-modal').classList.remove('flex');
}
function updateModalWeightDisplay(value) {
document.getElementById('modal-weight-value').textContent = parseFloat(value).toFixed(1);
}
async function submitPostType(event) {
event.preventDefault();
const id = document.getElementById('edit-id').value;
const name = document.getElementById('edit-name').value.trim();
const description = document.getElementById('edit-description').value.trim();
const strategyWeight = parseFloat(document.getElementById('edit-weight').value);
if (name.length < 3) {
showToast('Name muss mindestens 3 Zeichen lang sein', 'error');
return;
}
const activePostTypes = postTypesState.filter(pt => pt._status !== 'deleted');
const duplicateName = activePostTypes.find(pt => {
if (isEditMode && id && pt.id === id) return false;
return pt.name.toLowerCase() === name.toLowerCase();
});
if (duplicateName) {
showToast(`Ein Post-Typ mit dem Namen "${duplicateName.name}" existiert bereits`, 'error');
return;
}
if (isEditMode && id) {
updatePostTypeState(id, name, description, strategyWeight);
} else {
createPostTypeState(name, description, strategyWeight);
}
closeEditModal();
}
// ========== TOAST ==========
function showToast(message, type = 'info') {
const toast = document.createElement('div');
const colors = {
success: 'bg-green-600 border-green-500',
error: 'bg-red-600 border-red-500',
info: 'bg-blue-600 border-blue-500',
warning: 'bg-yellow-600 border-yellow-500'
};
const icons = {
success: '<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="M5 13l4 4L19 7"/></svg>',
error: '<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>',
info: '<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 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
warning: '<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>'
};
toast.className = `fixed bottom-8 right-8 ${colors[type]} text-white px-6 py-4 rounded-xl shadow-2xl border-2 z-50 flex items-center gap-3 transform transition-all duration-300 translate-y-0 opacity-100`;
toast.innerHTML = `${icons[type]}<span class="font-medium">${message}</span>`;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('translate-y-2', 'opacity-0');
setTimeout(() => toast.remove(), 300);
}, 4000);
}
</script>
<!-- Save All Button (Fixed) -->
{% if post_types_with_counts %}
<div class="fixed bottom-8 right-8 z-50">
<button id="save-all-btn"
onclick="saveAllChanges()"
disabled
class="bg-brand-highlight hover:bg-brand-highlight-dark text-brand-bg-dark font-bold px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
<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="M5 13l4 4L19 7"/>
</svg>
<span>Änderungen speichern</span>
</button>
</div>
{% endif %}
<!-- Create/Edit Modal (Wizard-Style) -->
<div id="edit-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden items-center justify-center z-50">
<div class="bg-brand-bg-dark border border-gray-600 rounded-xl p-8 w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white" id="modal-title">Neuer Post-Typ</h2>
<button onclick="closeEditModal()" class="text-gray-400 hover:text-white">
<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>
<!-- Step 1: Choose Predefined or Custom -->
<div id="modal-step-1" class="modal-step">
<p class="text-gray-400 mb-6">Wähle einen vorgefertigten Post-Typ oder erstelle einen eigenen</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<button type="button" onclick="selectPredefinedType('Tough Leadership')" class="predefined-type-card p-6 bg-brand-bg/50 hover:bg-brand-bg border-2 border-transparent hover:border-brand-highlight rounded-xl text-left transition-all">
<h3 class="text-lg font-bold text-white mb-2">Tough Leadership</h3>
<p class="text-sm text-gray-400">Meinungsbeiträge und Einblicke zu Branchenthemen, eigene Perspektiven und Expertenwissen</p>
</button>
<button type="button" onclick="selectPredefinedType('Company Updates')" class="predefined-type-card p-6 bg-brand-bg/50 hover:bg-brand-bg border-2 border-transparent hover:border-brand-highlight rounded-xl text-left transition-all">
<h3 class="text-lg font-bold text-white mb-2">Company Updates</h3>
<p class="text-sm text-gray-400">Neuigkeiten über das Unternehmen, Meilensteine, Produktankündigungen und Teamerfolge</p>
</button>
<button type="button" onclick="selectPredefinedType('Educational Content')" class="predefined-type-card p-6 bg-brand-bg/50 hover:bg-brand-bg border-2 border-transparent hover:border-brand-highlight rounded-xl text-left transition-all">
<h3 class="text-lg font-bold text-white mb-2">Educational Content</h3>
<p class="text-sm text-gray-400">Tutorials, How-Tos, Tipps & Tricks, Wissen teilen und Mehrwert bieten</p>
</button>
<button type="button" onclick="selectPredefinedType('Personal Story')" class="predefined-type-card p-6 bg-brand-bg/50 hover:bg-brand-bg border-2 border-transparent hover:border-brand-highlight rounded-xl text-left transition-all">
<h3 class="text-lg font-bold text-white mb-2">Personal Story</h3>
<p class="text-sm text-gray-400">Persönliche Erfahrungen, Learnings, Karrieremomente und authentische Geschichten</p>
</button>
<button type="button" onclick="selectPredefinedType('Industry News')" class="predefined-type-card p-6 bg-brand-bg/50 hover:bg-brand-bg border-2 border-transparent hover:border-brand-highlight rounded-xl text-left transition-all">
<h3 class="text-lg font-bold text-white mb-2">Industry News</h3>
<p class="text-sm text-gray-400">Aktuelle Branchennews, Trends, Marktentwicklungen und relevante Ereignisse</p>
</button>
<button type="button" onclick="selectPredefinedType('Engagement Post')" class="predefined-type-card p-6 bg-brand-bg/50 hover:bg-brand-bg border-2 border-transparent hover:border-brand-highlight rounded-xl text-left transition-all">
<h3 class="text-lg font-bold text-white mb-2">Engagement Post</h3>
<p class="text-sm text-gray-400">Umfragen, Fragen an die Community, Diskussionsanregungen und interaktive Inhalte</p>
</button>
</div>
<div class="border-t border-brand-bg-light pt-6">
<button type="button" onclick="goToCustomType()" class="w-full p-6 bg-brand-highlight/10 hover:bg-brand-highlight/20 border-2 border-brand-highlight/30 hover:border-brand-highlight rounded-xl text-center transition-all">
<svg class="w-8 h-8 mx-auto mb-2 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
<p class="text-white font-medium">Eigenen Post-Typ erstellen</p>
</button>
</div>
<div class="flex justify-end gap-3 mt-6">
<button type="button" onclick="closeEditModal()" class="px-6 py-2.5 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors">Abbrechen</button>
</div>
</div>
<!-- Step 2: Configure Type -->
<form id="edit-form" onsubmit="submitPostType(event)" class="modal-step hidden">
<input type="hidden" id="edit-id" value="">
<div class="mb-6">
<label class="block text-sm font-medium text-gray-300 mb-2">Name *</label>
<input type="text" id="edit-name" required minlength="3"
class="w-full input-bg border rounded-lg px-4 py-3 text-white text-lg"
placeholder="z.B. Tough Leadership">
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-300 mb-2">Beschreibung</label>
<textarea id="edit-description" rows="4"
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
placeholder="Beschreibe, worum es bei diesem Post-Typ geht..."></textarea>
</div>
<div class="mb-6 bg-brand-bg/30 rounded-lg p-5 border border-brand-bg-light">
<label class="block text-sm font-medium text-gray-300 mb-3 flex items-center justify-between">
<span>Strategy Weight</span>
<span class="text-brand-highlight font-bold text-xl" id="modal-weight-value">0.5</span>
</label>
<input type="range" id="edit-weight" min="0" max="1" step="0.1" value="0.5"
oninput="updateModalWeightDisplay(this.value)"
class="w-full h-3 bg-brand-bg-dark rounded-lg appearance-none cursor-pointer slider">
<div class="flex justify-between text-xs text-gray-400 mt-2">
<span>Ignorieren (0.0)</span>
<span>Ausgewogen (0.5)</span>
<span>Strikt (1.0)</span>
</div>
</div>
<div class="flex gap-3">
<button type="button" onclick="backToStep1()" class="px-6 py-3 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors">Zurück</button>
<button type="submit" class="flex-1 px-6 py-3 bg-brand-highlight hover:bg-brand-highlight-dark text-brand-bg-dark font-bold rounded-lg transition-colors">
Post-Typ speichern
</button>
</div>
</form>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden items-center justify-center z-50">
<div class="bg-brand-bg-dark border border-red-500/50 rounded-xl p-6 w-full max-w-md mx-4">
<div class="flex items-start gap-4 mb-6">
<div class="w-12 h-12 bg-red-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-red-400" 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>
<div>
<h2 class="text-xl font-bold text-white mb-2">Post-Typ löschen?</h2>
<p class="text-gray-300" id="delete-message">Möchtest du diesen Post-Typ wirklich löschen?</p>
</div>
</div>
<input type="hidden" id="delete-id" value="">
<div class="flex gap-3">
<button onclick="closeDeleteModal()" class="flex-1 px-4 py-2.5 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors">Abbrechen</button>
<button onclick="confirmDelete()" class="flex-1 px-4 py-2.5 bg-red-600 hover:bg-red-500 text-white font-medium rounded-lg transition-colors">Trotzdem löschen</button>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmation-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden items-center justify-center z-50">
<div class="bg-brand-bg-dark border border-gray-600 rounded-xl p-8 w-full max-w-md mx-4 animate-scale-in">
<div class="flex items-start gap-4 mb-6">
<div id="confirmation-icon" class="w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0"></div>
<div class="flex-1">
<h3 id="confirmation-title" class="text-xl font-bold text-white mb-2"></h3>
<p id="confirmation-message" class="text-gray-300 text-sm"></p>
</div>
</div>
<div class="flex gap-3">
<button onclick="closeConfirmationModal()" class="flex-1 px-4 py-2.5 bg-brand-bg-light hover:bg-brand-bg text-white font-medium rounded-lg transition-colors border border-gray-600">Abbrechen</button>
<button id="confirmation-confirm-btn" onclick="executeConfirmation()" class="flex-1 px-4 py-2.5 font-medium rounded-lg transition-colors">Bestätigen</button>
</div>
</div>
</div>
<style>
@keyframes scale-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.animate-scale-in { animation: scale-in 0.2s ease-out; }
.slider { -webkit-appearance: none; appearance: none; }
.slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 24px; height: 24px; border-radius: 50%;
background: linear-gradient(135deg, #facc15 0%, #fbbf24 100%);
cursor: pointer; box-shadow: 0 2px 8px rgba(250, 204, 21, 0.4);
border: 3px solid #1a1f1f; transition: all 0.2s ease;
}
.slider::-webkit-slider-thumb:hover { box-shadow: 0 4px 12px rgba(250, 204, 21, 0.6); transform: scale(1.1); }
.slider::-moz-range-thumb {
width: 24px; height: 24px; border-radius: 50%;
background: linear-gradient(135deg, #facc15 0%, #fbbf24 100%);
cursor: pointer; box-shadow: 0 2px 8px rgba(250, 204, 21, 0.4);
border: 3px solid #1a1f1f; transition: all 0.2s ease;
}
.slider::-webkit-slider-track { background: linear-gradient(to right, #4b5563 0%, #facc15 50%, #10b981 100%); border-radius: 8px; }
.slider::-moz-range-track { background: linear-gradient(to right, #4b5563 0%, #facc15 50%, #10b981 100%); border-radius: 8px; }
</style>
<script>
// State management
let postTypesState = [];
let originalPostTypesJSON = '';
let hasUnsavedChanges = false;
let isEditMode = false;
let currentEditId = null;
let tempIdCounter = 0;
let isIntentionalNavigation = false;
let confirmationCallback = null;
function initializeState() {
if (typeof INITIAL_POST_TYPES === 'undefined') {
showToast('Fehler: Daten konnten nicht geladen werden', 'error');
return;
}
try {
const data = INITIAL_POST_TYPES;
if (!Array.isArray(data)) throw new Error('Data is not an array');
postTypesState = data.map(item => ({
id: item.post_type.id,
name: item.post_type.name,
description: item.post_type.description,
strategy_weight: item.post_type.strategy_weight,
post_count: item.post_count,
_status: 'existing',
_originalWeight: item.post_type.strategy_weight
}));
originalPostTypesJSON = JSON.stringify(postTypesState);
renderPostTypesList();
updateSaveButton();
} catch (e) {
showToast('Fehler beim Laden der Post-Typen: ' + e.message, 'error');
}
}
function checkForChanges() {
const currentJSON = JSON.stringify(postTypesState.map(pt => ({
id: pt.id, name: pt.name, description: pt.description,
strategy_weight: pt.strategy_weight, _status: pt._status
})));
hasUnsavedChanges = currentJSON !== originalPostTypesJSON;
updateSaveButton();
}
function renderPostTypesList() {
const container = document.getElementById('post-types-list');
if (!container) return;
const activePostTypes = postTypesState.filter(pt => pt._status !== 'deleted');
if (activePostTypes.length === 0) {
container.innerHTML = `
<div class="card-bg border rounded-xl p-12 text-center">
<div class="w-20 h-20 bg-brand-highlight/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-10 h-10 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-2">Noch keine Post-Typen definiert</h3>
<p class="text-gray-400 mb-6">Erstelle den ersten Post-Typ.</p>
</div>`;
return;
}
container.innerHTML = activePostTypes.map(pt => {
const isNew = pt._status === 'new';
const isModified = pt._status === 'modified';
const statusBadge = isNew
? '<span class="text-xs bg-green-600/20 text-green-400 px-2 py-1 rounded ml-2">Neu</span>'
: isModified
? '<span class="text-xs bg-yellow-600/20 text-yellow-400 px-2 py-1 rounded ml-2">Geändert</span>'
: '';
return `
<div class="card-bg border rounded-xl p-6 ${isNew ? 'border-green-500/50' : isModified ? 'border-yellow-500/50' : ''}" data-post-type-id="${pt.id}">
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<h3 class="text-lg font-bold text-white mb-1">${escapeHtml(pt.name)}${statusBadge}</h3>
${pt.description ? `<p class="text-gray-400 text-sm">${escapeHtml(pt.description)}</p>` : ''}
</div>
<div class="flex gap-2">
<button onclick="editPostTypeState('${pt.id}')"
class="px-3 py-2 text-sm text-blue-400 hover:text-blue-300 hover:bg-blue-900/20 rounded-lg transition-colors">
Bearbeiten
</button>
<button onclick="deletePostTypeState('${pt.id}')"
class="px-3 py-2 text-sm text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded-lg transition-colors">
Löschen
</button>
</div>
</div>
<div class="mb-4 bg-brand-bg/30 rounded-lg p-4 border border-brand-bg-light">
<label class="text-sm font-medium text-gray-300 mb-3 block flex items-center justify-between">
<span>Strategy Weight</span>
<span class="text-brand-highlight font-bold text-lg" id="weight-value-${pt.id}">${pt.strategy_weight.toFixed(1)}</span>
</label>
<input type="range" id="weight-${pt.id}" min="0" max="1" step="0.1" value="${pt.strategy_weight}"
oninput="updateStrategyWeightState('${pt.id}', this.value)"
class="w-full h-3 bg-brand-bg-dark rounded-lg appearance-none cursor-pointer slider">
<div class="flex justify-between text-xs text-gray-400 mt-2">
<span>Ignorieren (0.0)</span><span>Ausgewogen (0.5)</span><span>Strikt (1.0)</span>
</div>
</div>
<div class="flex items-center gap-2 text-sm text-gray-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="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>
<span><strong>${pt.post_count || 0}</strong> Posts zugeordnet</span>
</div>
</div>`;
}).join('');
}
function updateSaveButton() {
const saveBtn = document.getElementById('save-all-btn');
if (!saveBtn) return;
const hasStructuralChanges = postTypesState.some(pt => pt._status === 'new' || pt._status === 'deleted');
const hasWeightChanges = postTypesState.some(pt => pt._status === 'modified' || (pt._status === 'existing' && pt.strategy_weight !== pt._originalWeight));
if (hasUnsavedChanges || hasStructuralChanges || hasWeightChanges) {
saveBtn.disabled = false;
saveBtn.classList.add('ring-4', 'ring-brand-highlight/50');
const btnText = saveBtn.querySelector('span');
if (btnText) btnText.textContent = hasStructuralChanges ? 'Speichern & Re-Kategorisieren' : 'Änderungen speichern';
} else {
saveBtn.disabled = true;
saveBtn.classList.remove('ring-4', 'ring-brand-highlight/50');
}
}
function createPostTypeState(name, description, strategyWeight) {
const tempId = `temp_${++tempIdCounter}`;
postTypesState.push({ id: tempId, name, description, strategy_weight: strategyWeight, post_count: 0, _status: 'new', _originalWeight: strategyWeight });
renderPostTypesList();
checkForChanges();
}
function editPostTypeState(id) {
const postType = postTypesState.find(pt => pt.id === id);
if (!postType) return;
document.getElementById('modal-title').textContent = 'Post-Typ bearbeiten';
document.getElementById('edit-id').value = id;
document.getElementById('edit-name').value = postType.name;
document.getElementById('edit-description').value = postType.description || '';
document.getElementById('edit-weight').value = postType.strategy_weight;
updateModalWeightDisplay(postType.strategy_weight);
isEditMode = true;
currentEditId = id;
document.getElementById('modal-step-1').classList.add('hidden');
document.getElementById('edit-form').classList.remove('hidden');
document.getElementById('edit-modal').classList.remove('hidden');
document.getElementById('edit-modal').classList.add('flex');
}
function updatePostTypeState(id, name, description, strategyWeight) {
const postType = postTypesState.find(pt => pt.id === id);
if (!postType) return;
postType.name = name;
postType.description = description;
postType.strategy_weight = strategyWeight;
if (postType._status === 'existing') postType._status = 'modified';
renderPostTypesList();
checkForChanges();
}
function deletePostTypeState(id) {
const activeTypes = postTypesState.filter(pt => pt._status !== 'deleted');
if (activeTypes.length === 1) {
showToast('Mindestens ein Post-Typ muss erhalten bleiben!', 'error');
return;
}
const postType = postTypesState.find(pt => pt.id === id);
if (!postType) return;
if (postType.post_count > 0) {
showConfirmation({
type: 'danger',
title: 'Post-Typ löschen?',
message: `Dieser Post-Typ hat ${postType.post_count} zugeordnete Posts. Beim Speichern werden diese Posts neu kategorisiert.`,
confirmText: 'Löschen',
onConfirm: () => performDelete(id)
});
} else {
performDelete(id);
}
}
function closeDeleteModal() {
document.getElementById('delete-modal').classList.add('hidden');
document.getElementById('delete-modal').classList.remove('flex');
}
function performDelete(id) {
const postType = postTypesState.find(pt => pt.id === id);
if (!postType) return;
if (postType._status === 'new') {
postTypesState = postTypesState.filter(pt => pt.id !== id);
} else {
postType._status = 'deleted';
}
renderPostTypesList();
checkForChanges();
}
function updateStrategyWeightState(id, value) {
const postType = postTypesState.find(pt => pt.id === id);
if (!postType) return;
const numValue = parseFloat(value);
postType.strategy_weight = numValue;
document.getElementById(`weight-value-${id}`).textContent = numValue.toFixed(1);
if (postType._status === 'existing' && numValue !== postType._originalWeight) postType._status = 'modified';
checkForChanges();
}
async function saveAllChanges() {
const saveBtn = document.getElementById('save-all-btn');
if (!saveBtn) return;
const activeTypes = postTypesState.filter(pt => pt._status !== 'deleted');
if (activeTypes.length === 0) {
showToast('Mindestens ein Post-Typ muss erhalten bleiben!', 'error');
return;
}
saveBtn.disabled = true;
showToast('Speichere Änderungen...', 'info');
try {
const hasStructuralChanges = postTypesState.some(pt => pt._status === 'new' || pt._status === 'deleted');
// 1. Delete removed post types
const deletedTypes = postTypesState.filter(pt => pt._status === 'deleted' && !pt.id.startsWith('temp_'));
for (const pt of deletedTypes) {
await fetch(`/api/company/manage/post-types/${pt.id}?employee_id=${EMPLOYEE_ID}`, { method: 'DELETE' });
}
// 2. Create new post types
const newTypes = postTypesState.filter(pt => pt._status === 'new');
for (const pt of newTypes) {
const response = await fetch('/api/company/manage/post-types', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employee_id: EMPLOYEE_ID, name: pt.name, description: pt.description, strategy_weight: pt.strategy_weight })
});
const result = await response.json();
if (!result.success) throw new Error(result.error || 'Failed to create post type');
}
// 3. Update modified post types
const modifiedTypes = postTypesState.filter(pt => pt._status === 'modified' && !pt.id.startsWith('temp_'));
for (const pt of modifiedTypes) {
await fetch(`/api/company/manage/post-types/${pt.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employee_id: EMPLOYEE_ID, name: pt.name, description: pt.description, strategy_weight: pt.strategy_weight })
});
}
// 4. Trigger save-all / re-categorization
const saveAllResponse = await fetch('/api/company/manage/post-types/save-all', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employee_id: EMPLOYEE_ID, has_structural_changes: hasStructuralChanges })
});
const saveAllResult = await saveAllResponse.json();
if (saveAllResult.success) {
postTypesState = postTypesState.filter(pt => pt._status !== 'deleted');
postTypesState.forEach(pt => { pt._status = 'existing'; pt._originalWeight = pt.strategy_weight; });
originalPostTypesJSON = JSON.stringify(postTypesState);
hasUnsavedChanges = false;
renderPostTypesList();
updateSaveButton();
showToast(saveAllResult.recategorized ? 'Gespeichert Rekategorisierung läuft im Hintergrund' : 'Änderungen gespeichert', 'success');
} else {
throw new Error(saveAllResult.error || 'Save failed');
}
} catch (error) {
showToast('Fehler beim Speichern: ' + error.message, 'error');
saveBtn.disabled = false;
}
}
// ========== CONFIRMATION MODAL ==========
function showConfirmation(options) {
const modal = document.getElementById('confirmation-modal');
const icon = document.getElementById('confirmation-icon');
const title = document.getElementById('confirmation-title');
const message = document.getElementById('confirmation-message');
const confirmBtn = document.getElementById('confirmation-confirm-btn');
title.textContent = options.title || 'Bestätigung';
message.textContent = options.message || 'Bist du sicher?';
const iconType = options.type || 'warning';
if (iconType === 'danger') {
icon.className = 'w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 bg-red-500/20';
icon.innerHTML = `<svg class="w-6 h-6 text-red-400" 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>`;
confirmBtn.className = 'flex-1 px-4 py-2.5 font-medium rounded-lg transition-colors bg-red-600 hover:bg-red-500 text-white';
} else {
icon.className = 'w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 bg-yellow-500/20';
icon.innerHTML = `<svg class="w-6 h-6 text-yellow-400" 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>`;
confirmBtn.className = 'flex-1 px-4 py-2.5 font-medium rounded-lg transition-colors bg-brand-highlight hover:bg-yellow-500 text-black';
}
confirmBtn.textContent = options.confirmText || 'Bestätigen';
confirmationCallback = options.onConfirm || null;
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeConfirmationModal() {
document.getElementById('confirmation-modal').classList.add('hidden');
document.getElementById('confirmation-modal').classList.remove('flex');
confirmationCallback = null;
}
function executeConfirmation() {
if (confirmationCallback) confirmationCallback();
closeConfirmationModal();
}
function handleBackNavigation(event) {
if (hasUnsavedChanges) {
event.preventDefault();
showConfirmation({
type: 'warning',
title: 'Ungespeicherte Änderungen',
message: 'Du hast ungespeicherte Änderungen. Wenn du jetzt zurückgehst, gehen diese verloren.',
confirmText: 'Trotzdem zurück',
onConfirm: () => {
isIntentionalNavigation = true;
window.location.href = `/company/manage/posts?employee_id=${EMPLOYEE_ID}`;
}
});
return false;
}
return true;
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
document.addEventListener('DOMContentLoaded', () => {
initializeState();
window.addEventListener('beforeunload', (e) => {
if (hasUnsavedChanges && !isIntentionalNavigation) {
e.preventDefault();
e.returnValue = '';
}
});
});
</script>
{% endblock %}

View File

@@ -104,12 +104,14 @@
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<div> <div>
<h1 class="text-2xl font-bold text-white mb-1">Posts von {{ employee_name }}</h1> <h1 class="text-2xl font-bold text-white mb-1">Posts von {{ employee_name }}</h1>
<p class="text-gray-400 text-sm">Ziehe Posts zwischen den Spalten um den Status zu ändern</p> <p class="text-gray-400 text-sm">Ziehe Posts zwischen den Spalten um den Status zu ändern. Die Spalte "Freigegeben" ist nur lesbar.</p>
</div> </div>
{% if not permissions or permissions.can_create_posts %}
<a href="/company/manage/create?employee_id={{ employee_id }}" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2"> <a href="/company/manage/create?employee_id={{ employee_id }}" 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> <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 Neuer Post
</a> </a>
{% endif %}
</div> </div>
<div class="kanban-board"> <div class="kanban-board">
@@ -155,20 +157,40 @@
</div> </div>
</div> </div>
<!-- Column: Freigegeben (ready) - approved by customer, ready for calendar scheduling --> <!-- Column: Freigegeben (ready) - approved by employee, read-only for company -->
<div class="kanban-column column-ready"> <div class="kanban-column column-ready" style="opacity: 0.85;">
<div class="kanban-header"> <div class="kanban-header">
<h3 class="text-green-400"> <h3 class="text-green-400 opacity-70 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 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> <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 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Freigegeben Freigegeben
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
<span class="text-xs text-gray-500 font-normal">(Nur Ansicht)</span>
</h3> </h3>
<span class="kanban-count" id="count-ready">{{ posts | selectattr('status', 'equalto', 'ready') | list | length }}</span> <span class="kanban-count" id="count-ready">{{ posts | selectattr('status', 'equalto', 'ready') | list | length }}</span>
</div> </div>
<div class="kanban-body" data-status="ready" ondragover="handleDragOver(event)" ondrop="handleDrop(event)" ondragleave="handleDragLeave(event)"> <div class="kanban-body" data-status="ready" style="pointer-events: none;">
{% for post in posts if post.status == 'ready' %} {% for post in posts if post.status == 'ready' %}
{{ render_post_card(post) }} <div class="post-card" data-post-id="{{ post.id }}" style="cursor: pointer; pointer-events: all;"
onclick="window.location.href='/company/manage/post/{{ post.id }}?employee_id={{ employee_id }}'">
<div class="flex items-start justify-between gap-2 mb-2">
<h4 class="post-card-title">{{ post.topic_title or 'Untitled' }}</h4>
{% if post.critic_feedback and post.critic_feedback | length > 0 and post.critic_feedback[-1].get('overall_score') is not none %}
{% set score = post.critic_feedback[-1].get('overall_score', 0) %}
<span class="score-badge flex-shrink-0 {{ 'score-high' if score >= 85 else 'score-medium' if score >= 70 else 'score-low' }}">{{ score }}</span>
{% endif %}
</div>
<div class="post-card-meta">
<span class="flex items-center gap-1">
<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>
</div>
{% if post.post_content %}
<p class="post-card-preview">{{ post.post_content[:150] }}{% if post.post_content | length > 150 %}...{% endif %}</p>
{% endif %}
</div>
{% else %} {% else %}
<div class="empty-column" id="empty-ready"> <div class="empty-column" id="empty-ready" style="pointer-events: all;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<p>Keine freigegebenen Posts</p> <p>Keine freigegebenen Posts</p>
</div> </div>
@@ -194,6 +216,22 @@
{% block scripts %} {% block scripts %}
<script> <script>
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 text-sm ${
type === 'success' ? 'bg-green-600 text-white' :
type === 'error' ? 'bg-red-600 text-white' :
'bg-gray-700 text-white'
}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.3s';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
let draggedElement = null; let draggedElement = null;
let sourceStatus = null; let sourceStatus = null;
@@ -233,6 +271,12 @@ async function handleDrop(e) {
if (sourceStatus === newStatus) return; if (sourceStatus === newStatus) return;
// Company cannot drag posts to "Freigegeben" — only employees can
if (newStatus === 'ready') {
showToast('Nur Mitarbeiter können Posts freigeben', 'error');
return;
}
const emptyPlaceholder = kanbanBody.querySelector('.empty-column'); const emptyPlaceholder = kanbanBody.querySelector('.empty-column');
if (emptyPlaceholder) emptyPlaceholder.remove(); if (emptyPlaceholder) emptyPlaceholder.remove();

View File

@@ -0,0 +1,331 @@
{% extends "base.html" %}
{% block title %}Mein Kalender{% endblock %}
{% block head %}
<style>
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #5a6868;
}
.calendar-cell {
background: #4a5858;
min-height: 120px;
padding: 8px;
}
.calendar-cell.other-month {
background: #3d4848;
opacity: 0.5;
}
.calendar-cell.today {
border: 2px solid #ffc700;
}
.post-chip {
font-size: 11px;
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 4px;
cursor: pointer;
transition: transform 0.1s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.post-chip:hover { transform: scale(1.02); }
.post-chip.scheduled { background: #ffc700; color: #2d3838; }
.post-chip.published { background: #6b7280; color: #9ca3af; opacity: 0.6; }
.post-chip.approved { background: #3b82f6; color: white; }
.post-chip.ready { background: #22c55e; color: white; }
.view-toggle .active { background: #ffc700; color: #2d3838; }
.timeline-header {
display: grid;
grid-template-columns: 50px repeat(7, 1fr);
background: #3d4848;
}
.timeline-header-cell {
padding: 8px 4px;
text-align: center;
font-size: 13px;
color: #9ca3af;
border-left: 1px solid #5a6868;
}
.timeline-header-cell.today { color: #ffc700; font-weight: bold; }
.timeline-grid {
display: grid;
grid-template-columns: 50px repeat(7, 1fr);
position: relative;
}
.timeline-hour-label {
font-size: 11px;
color: #6b7280;
text-align: right;
padding-right: 8px;
height: 60px;
line-height: 1;
padding-top: 0;
transform: translateY(-6px);
}
.timeline-day-col {
position: relative;
height: 60px;
border-top: 1px solid #5a6868;
border-left: 1px solid #5a6868;
}
.timeline-post {
position: absolute;
left: 2px;
right: 2px;
height: 36px;
border-radius: 4px;
padding: 2px 6px;
font-size: 10px;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
z-index: 5;
}
.timeline-post.scheduled { background: #ffc700; color: #2d3838; }
.timeline-post.approved { background: #3b82f6; color: white; }
.timeline-post.published { background: #374151; color: #9ca3af; border: 1px solid #6b7280; }
.timeline-post.ready { background: #22c55e; color: white; }
</style>
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-white">Mein Kalender</h1>
<p class="text-gray-400 mt-1">Deine geplanten und freigegebenen Posts</p>
</div>
<div class="flex items-center gap-4">
<!-- View Toggle -->
<div class="view-toggle flex rounded-lg overflow-hidden border border-gray-600">
<a href="/calendar?month={{ month }}&year={{ year }}&view=month"
class="px-3 py-1.5 text-sm transition-colors {% if view == 'month' %}active{% else %}text-gray-300 hover:bg-brand-bg-dark{% endif %}">
Monat
</a>
<a href="/calendar?view=week&month={{ month }}&year={{ year }}"
class="px-3 py-1.5 text-sm transition-colors {% if view == 'week' %}active{% else %}text-gray-300 hover:bg-brand-bg-dark{% endif %}">
Woche
</a>
</div>
<!-- Navigation -->
<div class="flex items-center gap-2">
{% if view == 'week' %}
<a href="/calendar?view=week&week_start={{ prev_week_start }}&month={{ month }}&year={{ year }}" class="p-2 rounded-lg bg-brand-bg-dark hover:bg-brand-bg-light transition-colors">
<svg class="w-5 h-5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
</a>
<span class="text-white font-medium min-w-[200px] text-center">{{ week_label }}</span>
<a href="/calendar?view=week&week_start={{ next_week_start }}&month={{ month }}&year={{ year }}" class="p-2 rounded-lg bg-brand-bg-dark hover:bg-brand-bg-light transition-colors">
<svg class="w-5 h-5 text-gray-300" 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>
</a>
{% else %}
<a href="/calendar?month={{ prev_month }}&year={{ prev_year }}&view=month" class="p-2 rounded-lg bg-brand-bg-dark hover:bg-brand-bg-light transition-colors">
<svg class="w-5 h-5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
</a>
<span class="text-white font-medium min-w-[150px] text-center">{{ month_name }} {{ year }}</span>
<a href="/calendar?month={{ next_month }}&year={{ next_year }}&view=month" class="p-2 rounded-lg bg-brand-bg-dark hover:bg-brand-bg-light transition-colors">
<svg class="w-5 h-5 text-gray-300" 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>
</a>
{% endif %}
</div>
{% if view == 'week' %}
<a href="/calendar?view=week" class="px-4 py-2 text-sm bg-brand-bg-dark hover:bg-brand-bg-light rounded-lg text-gray-300 transition-colors">Heute</a>
{% else %}
<a href="/calendar?month={{ current_month }}&year={{ current_year }}" class="px-4 py-2 text-sm bg-brand-bg-dark hover:bg-brand-bg-light rounded-lg text-gray-300 transition-colors">Heute</a>
{% endif %}
</div>
</div>
<!-- Status Legend -->
<div class="card-bg border rounded-xl p-4 mb-6">
<p class="text-sm text-gray-400 mb-3">Status</p>
<div class="flex flex-wrap gap-4">
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded" style="background:#3b82f6;"></div><span class="text-sm text-white">Bearbeitet</span></div>
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded" style="background:#22c55e;"></div><span class="text-sm text-white">Freigegeben</span></div>
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded" style="background:#ffc700;"></div><span class="text-sm text-white">Geplant</span></div>
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded" style="background:#6b7280;"></div><span class="text-sm text-white">Veröffentlicht</span></div>
</div>
</div>
<!-- Calendar -->
{% if view == 'week' %}
<div class="card-bg border rounded-xl overflow-hidden">
<div class="timeline-header">
<div class="timeline-header-cell" style="border-left: none;"></div>
{% for day in calendar_weeks[0] %}
<div class="timeline-header-cell {% if day.is_today %}today{% endif %}">{{ day.full_date }}</div>
{% endfor %}
</div>
<div class="timeline-grid" id="timelineGrid">
{% for hour in range(0, 24) %}
<div class="timeline-hour-label">{{ '%02d'|format(hour) }}:00</div>
{% for day in calendar_weeks[0] %}
<div class="timeline-day-col" data-date="{{ day.date }}" data-hour="{{ hour }}">
{% for post in day.posts %}
{% set post_hour = post.time[:2]|int %}
{% if post_hour == hour %}
<div class="timeline-post {{ post.status }}"
onclick="window.location.href='/posts/{{ post.id }}'"
title="{{ post.topic_title }}"
style="top: {{ post.time[3:]|int }}px;">
<div style="font-weight:600;font-size:10px;">{{ post.time }}</div>
<div style="font-size:10px;">{{ post.topic_title[:20] }}{% if post.topic_title|length > 20 %}...{% endif %}</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
{% endfor %}
</div>
</div>
{% else %}
<div class="card-bg border rounded-xl overflow-hidden">
<div class="grid grid-cols-7 bg-brand-bg-dark">
<div class="p-3 text-center text-gray-400 text-sm font-medium">Mo</div>
<div class="p-3 text-center text-gray-400 text-sm font-medium">Di</div>
<div class="p-3 text-center text-gray-400 text-sm font-medium">Mi</div>
<div class="p-3 text-center text-gray-400 text-sm font-medium">Do</div>
<div class="p-3 text-center text-gray-400 text-sm font-medium">Fr</div>
<div class="p-3 text-center text-gray-400 text-sm font-medium">Sa</div>
<div class="p-3 text-center text-gray-400 text-sm font-medium">So</div>
</div>
<div class="calendar-grid">
{% for week in calendar_weeks %}
{% for day in week %}
<div class="calendar-cell {% if day.other_month %}other-month{% endif %} {% if day.is_today %}today{% endif %}">
<div class="text-sm {% if day.is_today %}text-brand-highlight font-bold{% else %}text-gray-400{% endif %} mb-2">{{ day.day }}</div>
{% for post in day.posts %}
<div class="post-chip {{ post.status }}"
onclick="window.location.href='/posts/{{ post.id }}'"
title="{{ post.topic_title }}">
<span style="font-size:10px;color:inherit;opacity:0.8;">{{ post.time }}</span>
{{ post.topic_title[:20] }}{% if post.topic_title|length > 20 %}...{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
{% endfor %}
</div>
</div>
{% endif %}
<!-- Unscheduled Posts -->
{% if unscheduled_posts %}
<div class="card-bg border rounded-xl p-6 mt-6">
<h2 class="text-lg font-semibold text-white mb-4">Nicht eingeplante Posts</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for post in unscheduled_posts %}
<div class="bg-brand-bg-dark rounded-lg p-4 border border-transparent hover:border-brand-highlight transition-colors">
<div class="flex items-start justify-between gap-2 mb-2">
<span class="text-xs px-2 py-1 rounded {% if post.status == 'ready' %}bg-green-500/20 text-green-400{% else %}bg-blue-500/20 text-blue-400{% endif %} inline-block">{{ post.status }}</span>
{% if post.status == 'ready' %}
<button onclick="openScheduleModalForPost('{{ post.id }}', '{{ post.topic_title | replace("'", "\\'") }}')"
class="text-xs px-2 py-1 bg-brand-highlight text-brand-bg-dark rounded font-medium hover:bg-brand-highlight-dark transition-colors flex-shrink-0">
Planen
</button>
{% endif %}
</div>
<a href="/posts/{{ post.id }}" class="block">
<p class="text-white text-sm font-medium mt-1">{{ post.topic_title }}</p>
<p class="text-gray-400 text-xs line-clamp-2 mt-1">{{ post.post_content[:100] }}...</p>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Schedule Modal -->
<div id="scheduleModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-brand-bg-light rounded-xl p-6 w-full max-w-md mx-4">
<h3 class="text-lg font-semibold text-white mb-4">Post einplanen</h3>
<p id="schedulePostTitle" class="text-gray-300 text-sm mb-4"></p>
<form id="scheduleForm" onsubmit="submitSchedule(event)">
<input type="hidden" id="schedulePostId" name="post_id">
<div class="mb-4">
<label class="block text-sm text-gray-300 mb-2">Datum</label>
<input type="date" id="scheduleDate" name="date" required
class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div class="mb-6">
<label class="block text-sm text-gray-300 mb-2">Uhrzeit</label>
<input type="time" id="scheduleTime" name="time" value="09:00" required
class="w-full input-bg border rounded-lg px-4 py-2 text-white">
</div>
<div class="flex gap-3">
<button type="button" onclick="closeScheduleModal()" class="flex-1 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-500 transition-colors">
Abbrechen
</button>
<button type="submit" class="flex-1 py-2 bg-brand-highlight text-brand-bg-dark rounded-lg hover:bg-brand-highlight-dark transition-colors font-medium">
Einplanen
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function openScheduleModalForPost(postId, title) {
document.getElementById('schedulePostId').value = postId;
document.getElementById('schedulePostTitle').textContent = title ? `"${title}"` : '';
const today = new Date().toISOString().split('T')[0];
document.getElementById('scheduleDate').value = today;
document.getElementById('scheduleTime').value = '09:00';
document.getElementById('scheduleModal').classList.remove('hidden');
document.getElementById('scheduleModal').classList.add('flex');
}
function closeScheduleModal() {
document.getElementById('scheduleModal').classList.add('hidden');
document.getElementById('scheduleModal').classList.remove('flex');
}
async function submitSchedule(event) {
event.preventDefault();
const postId = document.getElementById('schedulePostId').value;
const date = document.getElementById('scheduleDate').value;
const time = document.getElementById('scheduleTime').value;
if (!postId) return;
const localDateTime = new Date(`${date}T${time}:00`);
if (localDateTime < new Date()) {
alert('Der gewählte Zeitpunkt liegt in der Vergangenheit.');
return;
}
try {
const formData = new FormData();
formData.append('scheduled_at', localDateTime.toISOString());
const response = await fetch(`/api/posts/${postId}/schedule`, {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Fehler beim Einplanen');
}
window.location.reload();
} catch (error) {
alert('Fehler: ' + error.message);
}
}
document.getElementById('scheduleModal').addEventListener('click', function(e) {
if (e.target === this) closeScheduleModal();
});
</script>
{% endblock %}

View File

@@ -909,6 +909,37 @@
</a> </a>
</div> </div>
</div> </div>
<!-- Schedule Section -->
<div class="section-card rounded-xl p-6">
<h3 class="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="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>
Planen
</h3>
{% if post.status == 'scheduled' and post.scheduled_at %}
<div class="mb-3 p-3 bg-brand-highlight/10 border border-brand-highlight/30 rounded-lg">
<p class="text-xs text-gray-400 mb-1">Geplant für</p>
<p class="text-white font-medium text-sm">{{ post.scheduled_at.strftime('%d.%m.%Y %H:%M') }} Uhr</p>
</div>
<button onclick="employeeUnschedulePost()" class="w-full px-4 py-2.5 bg-red-600/20 hover:bg-red-600/30 text-red-300 rounded-lg text-sm transition-colors flex items-center justify-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="M6 18L18 6M6 6l12 12"/></svg>
Planung aufheben
</button>
{% else %}
{% if post.status != 'ready' %}
<p class="text-xs text-gray-500 mb-3">Nur freigegebene Posts (Status "Freigegeben") können geplant werden.</p>
{% endif %}
<input type="datetime-local" id="scheduleInputEmployee"
class="w-full bg-brand-bg border border-gray-600 rounded-lg px-3 py-2 text-white text-sm mb-3 focus:outline-none focus:border-brand-highlight transition-colors {% if post.status != 'ready' %}opacity-50 cursor-not-allowed{% endif %}"
{% if post.status != 'ready' %}disabled{% endif %}>
<button onclick="employeeSchedulePost()" id="scheduleBtnEmployee"
class="w-full px-4 py-2.5 bg-brand-highlight hover:bg-yellow-500 text-brand-bg-dark font-medium rounded-lg text-sm transition-colors flex items-center justify-center gap-2 {% if post.status != 'ready' %}opacity-50 cursor-not-allowed{% endif %}"
{% if post.status != 'ready' %}disabled{% endif %}>
<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="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 planen
</button>
{% endif %}
</div>
</div> </div>
</div> </div>
@@ -2010,5 +2041,52 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
}); });
// ==================== EMPLOYEE SCHEDULING ====================
async function employeeSchedulePost() {
const input = document.getElementById('scheduleInputEmployee');
if (!input || !input.value) { showToast('Bitte Datum und Uhrzeit auswählen', 'error'); return; }
const btn = document.getElementById('scheduleBtnEmployee');
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;border-width:2px;border-top-color:#1a1a1a;border-color:rgba(0,0,0,0.2);border-top-color:#1a1a1a;"></div>';
try {
const localDate = new Date(input.value);
const formData = new FormData();
formData.append('scheduled_at', localDate.toISOString());
const response = await fetch(`/api/posts/${postId}/schedule`, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
showToast('Post erfolgreich geplant!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast('Fehler: ' + (result.detail || 'Planen fehlgeschlagen'), 'error');
}
} catch (e) {
showToast('Netzwerkfehler', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalHTML;
}
}
async function employeeUnschedulePost() {
if (!confirm('Planung aufheben?')) return;
try {
const response = await fetch(`/api/posts/${postId}/unschedule`, { method: 'POST' });
const result = await response.json();
if (result.success) {
showToast('Planung aufgehoben', 'success');
setTimeout(() => location.reload(), 800);
} else {
showToast('Fehler: ' + (result.detail || 'Aufheben fehlgeschlagen'), 'error');
}
} catch (e) {
showToast('Netzwerkfehler', 'error');
}
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -234,6 +234,103 @@
</div> </div>
{% endif %} {% endif %}
{% if session and session.account_type == 'employee' and session.company_id and company_permissions %}
<!-- Company Permissions -->
<div class="card-bg rounded-xl border border-gray-700 p-6 mb-6">
<h2 class="text-lg font-semibold text-white mb-2 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="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
Unternehmens-Berechtigungen
</h2>
<p class="text-sm text-gray-400 mb-6">
Lege fest, was <strong class="text-white">{{ company.name if company else 'dein Unternehmen' }}</strong> mit deinen Inhalten tun darf.
Standardmäßig sind alle Berechtigungen aktiviert.
</p>
<form id="companyPermissionsForm" class="space-y-4">
<div class="flex items-center justify-between py-3 border-b border-gray-700">
<div>
<p class="text-white text-sm font-medium">Posts erstellen</p>
<p class="text-gray-400 text-xs">Darf {{ company.name if company else 'das Unternehmen' }} neue Posts für mich erstellen</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="can_create_posts" class="sr-only peer" {% if company_permissions.can_create_posts %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-highlight"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-700">
<div>
<p class="text-white text-sm font-medium">Posts ansehen</p>
<p class="text-gray-400 text-xs">Darf {{ company.name if company else 'das Unternehmen' }} alle meine Posts sehen</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="can_view_posts" class="sr-only peer" {% if company_permissions.can_view_posts %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-highlight"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-700">
<div>
<p class="text-white text-sm font-medium">Posts bearbeiten</p>
<p class="text-gray-400 text-xs">Darf {{ company.name if company else 'das Unternehmen' }} meine Posts bearbeiten</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="can_edit_posts" class="sr-only peer" {% if company_permissions.can_edit_posts %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-highlight"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-700">
<div>
<p class="text-white text-sm font-medium">Posts schedulen</p>
<p class="text-gray-400 text-xs">Darf {{ company.name if company else 'das Unternehmen' }} meine Posts terminieren</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="can_schedule_posts" class="sr-only peer" {% if company_permissions.can_schedule_posts %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-highlight"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-700">
<div>
<p class="text-white text-sm font-medium">Post Typen verwalten</p>
<p class="text-gray-400 text-xs">Darf {{ company.name if company else 'das Unternehmen' }} meine Post-Typen verwalten</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="can_manage_post_types" class="sr-only peer" {% if company_permissions.can_manage_post_types %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-highlight"></div>
</label>
</div>
<div class="flex items-center justify-between py-3 border-b border-gray-700">
<div>
<p class="text-white text-sm font-medium">Recherche</p>
<p class="text-gray-400 text-xs">Darf {{ company.name if company else 'das Unternehmen' }} Recherche für mich durchführen</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="can_do_research" class="sr-only peer" {% if company_permissions.can_do_research %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-highlight"></div>
</label>
</div>
<div class="flex items-center justify-between py-3">
<div>
<p class="text-white text-sm font-medium">Kalender sichtbar</p>
<p class="text-gray-400 text-xs">Sind meine Posts im Unternehmens-Kalender sichtbar</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="can_see_in_calendar" class="sr-only peer" {% if company_permissions.can_see_in_calendar %}checked{% endif %}>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-highlight"></div>
</label>
</div>
<div class="pt-4">
<button type="submit" id="savePermissionsBtn"
class="px-6 py-3 bg-brand-highlight hover:bg-brand-highlight/90 text-brand-bg-dark font-medium 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="M5 13l4 4L19 7"/>
</svg>
Berechtigungen speichern
</button>
</div>
</form>
</div>
{% endif %}
<!-- Workflow Info --> <!-- Workflow Info -->
<div class="card-bg rounded-xl border p-6"> <div class="card-bg rounded-xl border p-6">
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2"> <h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
@@ -320,6 +417,58 @@ document.getElementById('emailSettingsForm').addEventListener('submit', async (e
} }
}); });
// Company permissions form
const permissionsForm = document.getElementById('companyPermissionsForm');
if (permissionsForm) {
permissionsForm.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('savePermissionsBtn');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<div class="w-4 h-4 border-2 border-brand-bg-dark border-t-transparent rounded-full animate-spin"></div> Speichern...';
btn.disabled = true;
try {
const formData = new FormData(permissionsForm);
// Ensure unchecked boxes are sent too (FormData only includes checked ones)
['can_create_posts','can_view_posts','can_edit_posts','can_schedule_posts','can_manage_post_types','can_do_research','can_see_in_calendar'].forEach(key => {
if (!formData.has(key)) {
// do not append — absence signals false
}
});
const response = await fetch('/api/settings/company-permissions', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Fehler beim Speichern');
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="M5 13l4 4L19 7"/></svg> Gespeichert!';
btn.classList.remove('bg-brand-highlight');
btn.classList.add('bg-green-600');
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove('bg-green-600');
btn.classList.add('bg-brand-highlight');
btn.disabled = false;
}, 2000);
} catch (error) {
console.error('Error saving permissions:', error);
btn.innerHTML = 'Fehler!';
btn.classList.remove('bg-brand-highlight');
btn.classList.add('bg-red-600');
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove('bg-red-600');
btn.classList.add('bg-brand-highlight');
btn.disabled = false;
}, 2000);
}
});
}
// Telegram connect // Telegram connect
async function connectTelegram() { async function connectTelegram() {
try { try {

File diff suppressed because it is too large Load Diff