major restructure + Permission system
This commit is contained in:
@@ -1640,6 +1640,45 @@ class DatabaseClient:
|
||||
)
|
||||
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
|
||||
db = DatabaseClient()
|
||||
|
||||
@@ -415,3 +415,19 @@ class CompanyDailyQuota(DBModel):
|
||||
tokens_used: int = 0
|
||||
created_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
|
||||
|
||||
@@ -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(
|
||||
to_email: str,
|
||||
post_title: str,
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
main.sidebar-collapsed { margin-left: 4rem; }
|
||||
.toggle-btn { cursor: pointer; transition: transform 0.3s ease; }
|
||||
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>
|
||||
|
||||
<!-- Prevent sidebar flash on page load -->
|
||||
@@ -110,7 +117,7 @@
|
||||
</div>
|
||||
{% 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 %}">
|
||||
<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>
|
||||
@@ -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>
|
||||
<span class="sidebar-text">Status</span>
|
||||
</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' %}
|
||||
<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>
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
main.sidebar-collapsed { margin-left: 4rem; }
|
||||
.toggle-btn { cursor: pointer; transition: transform 0.3s ease; }
|
||||
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>
|
||||
|
||||
<!-- Prevent sidebar flash on page load -->
|
||||
@@ -106,7 +113,7 @@
|
||||
</div>
|
||||
{% 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 %}">
|
||||
<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>
|
||||
|
||||
@@ -338,10 +338,16 @@
|
||||
</div>
|
||||
<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>
|
||||
{% if post.can_schedule %}
|
||||
<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">
|
||||
Einplanen
|
||||
</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>
|
||||
{% endfor %}
|
||||
</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">
|
||||
<option value="">-- Post auswahlen --</option>
|
||||
{% for post in unscheduled_posts %}
|
||||
{% if post.can_schedule %}
|
||||
<option value="{{ post.id }}">{{ post.employee_name }}: {{ post.topic_title[:40] }}...</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -57,14 +57,85 @@
|
||||
</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 -->
|
||||
<div class="grid md:grid-cols-3 gap-6 mb-8">
|
||||
<div class="card-bg border rounded-xl p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-brand-highlight/20 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-white">{{ employee_posts | length }}</p>
|
||||
@@ -72,13 +143,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-bg border rounded-xl p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-white">{{ pending_posts }}</p>
|
||||
@@ -86,13 +154,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-bg border rounded-xl p-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<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">
|
||||
<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-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>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-white">{{ approved_posts }}</p>
|
||||
@@ -102,46 +167,6 @@
|
||||
</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 -->
|
||||
{% if employee_posts %}
|
||||
<div class="card-bg border rounded-xl p-6">
|
||||
@@ -168,15 +193,15 @@
|
||||
{% else %}
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}<!-- end can_view_posts check -->
|
||||
|
||||
{% else %}
|
||||
<!-- No Employee Selected -->
|
||||
{% if active_employees %}
|
||||
|
||||
229
src/web/templates/user/company_manage_chat_create.html
Normal file
229
src/web/templates/user/company_manage_chat_create.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
LinkedIn Post
|
||||
</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">
|
||||
<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 class="flex items-center gap-2">
|
||||
{% if not permissions or permissions.can_edit_posts %}
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div id="previewSection">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<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>
|
||||
@@ -252,6 +285,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- end previewSection -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -276,6 +310,94 @@
|
||||
</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) -->
|
||||
<div class="section-card rounded-xl p-6">
|
||||
<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);">
|
||||
{% if 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' %}
|
||||
<img src="{{ item.url }}" alt="Media {{ loop.index }}" class="w-full h-48 object-cover">
|
||||
{% elif item.type == 'video' %}
|
||||
<video src="{{ item.url }}" class="w-full h-48 object-cover" controls></video>
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<!-- Order badge -->
|
||||
<div class="absolute bottom-2 left-2 px-2 py-1 bg-black/50 rounded text-xs text-white">
|
||||
@@ -310,7 +435,8 @@
|
||||
{% endif %}
|
||||
</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">
|
||||
<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>
|
||||
@@ -326,6 +452,11 @@
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1 text-center">Wird hochgeladen...</p>
|
||||
</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>
|
||||
|
||||
<!-- Post Info -->
|
||||
@@ -342,8 +473,14 @@
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<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>
|
||||
{% 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 %}
|
||||
<div class="pt-3 border-t border-brand-bg-light">
|
||||
<span class="text-gray-400 block mb-1">Topic</span>
|
||||
@@ -360,6 +497,23 @@
|
||||
<script>
|
||||
const POST_ID = '{{ post.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() {
|
||||
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)
|
||||
function setPreviewMode(mode) {
|
||||
const preview = document.getElementById('linkedinPreview');
|
||||
@@ -416,21 +692,178 @@ async function updateStatus(newStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== IMAGE UPLOAD ====================
|
||||
// ==================== AI SUGGESTIONS ====================
|
||||
|
||||
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 transition-all transform translate-y-0 opacity-100 ${
|
||||
type === 'success' ? 'bg-green-600 text-white' :
|
||||
type === 'error' ? 'bg-red-600 text-white' :
|
||||
'bg-brand-bg-light text-white'
|
||||
}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-2');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
async function loadSuggestions() {
|
||||
const loadingEl = document.getElementById('suggestionsLoading');
|
||||
const listEl = document.getElementById('suggestionsList');
|
||||
const quickSuggestions = document.getElementById('quickSuggestions');
|
||||
const refreshBtn = document.getElementById('refreshSuggestionsBtn');
|
||||
if (!loadingEl) return;
|
||||
|
||||
loadingEl.classList.remove('hidden');
|
||||
quickSuggestions.classList.add('hidden');
|
||||
listEl.classList.add('hidden');
|
||||
listEl.innerHTML = '';
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
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 ====================
|
||||
@@ -552,24 +985,26 @@ function refreshMediaGrid() {
|
||||
const isDragged = draggedItemData && item.url === draggedItemData.url;
|
||||
const ghostClass = isDragged ? 'is-ghost' : '';
|
||||
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 `
|
||||
<div class="media-item relative group rounded-lg overflow-hidden border-2 border-transparent hover:border-brand-highlight ${ghostClass}"
|
||||
data-array-index="${i}"
|
||||
draggable="true"
|
||||
style="cursor: grab; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); ${ghostStyle}">
|
||||
${CAN_EDIT ? 'draggable="true"' : ''}
|
||||
style="${CAN_EDIT ? 'cursor: grab;' : 'cursor: default;'} transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); ${ghostStyle}">
|
||||
<div class="media-content">
|
||||
${item.type === 'image'
|
||||
? `<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>`
|
||||
}
|
||||
</div>
|
||||
<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>
|
||||
${deleteBtn}
|
||||
<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>
|
||||
`;
|
||||
@@ -739,6 +1174,7 @@ async function saveReorderInBackground() {
|
||||
}
|
||||
|
||||
function initMediaUpload() {
|
||||
if (!CAN_EDIT) return;
|
||||
const uploadZone = document.getElementById('mediaUploadZone');
|
||||
const fileInput = document.getElementById('mediaFileInput');
|
||||
|
||||
@@ -768,6 +1204,21 @@ function initMediaUpload() {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
694
src/web/templates/user/company_manage_post_types.html
Normal file
694
src/web/templates/user/company_manage_post_types.html
Normal 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 %}
|
||||
@@ -104,12 +104,14 @@
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
{% 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">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
Neuer Post
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="kanban-board">
|
||||
@@ -155,20 +157,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column: Freigegeben (ready) - approved by customer, ready for calendar scheduling -->
|
||||
<div class="kanban-column column-ready">
|
||||
<!-- Column: Freigegeben (ready) - approved by employee, read-only for company -->
|
||||
<div class="kanban-column column-ready" style="opacity: 0.85;">
|
||||
<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>
|
||||
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>
|
||||
<span class="kanban-count" id="count-ready">{{ posts | selectattr('status', 'equalto', 'ready') | list | length }}</span>
|
||||
</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' %}
|
||||
{{ 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 %}
|
||||
<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>
|
||||
<p>Keine freigegebenen Posts</p>
|
||||
</div>
|
||||
@@ -194,6 +216,22 @@
|
||||
|
||||
{% block scripts %}
|
||||
<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 sourceStatus = null;
|
||||
|
||||
@@ -233,6 +271,12 @@ async function handleDrop(e) {
|
||||
|
||||
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');
|
||||
if (emptyPlaceholder) emptyPlaceholder.remove();
|
||||
|
||||
|
||||
331
src/web/templates/user/employee_calendar.html
Normal file
331
src/web/templates/user/employee_calendar.html
Normal 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 %}
|
||||
@@ -909,6 +909,37 @@
|
||||
</a>
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -234,6 +234,103 @@
|
||||
</div>
|
||||
{% 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 -->
|
||||
<div class="card-bg rounded-xl border p-6">
|
||||
<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
|
||||
async function connectTelegram() {
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user