@@ -287,19 +409,22 @@
Zeichen
- {{ post.post_content | length }}
+ {{ post.post_content | length }}
+ {% if post.scheduled_at %}
+
+ Geplant
+ {{ post.scheduled_at.strftime('%d.%m.%Y %H:%M') }}
+
+ {% endif %}
{% if post.topic_title %}
Topic
@@ -360,6 +497,23 @@
{% endblock %}
diff --git a/src/web/templates/user/company_manage_post_types.html b/src/web/templates/user/company_manage_post_types.html
new file mode 100644
index 0000000..5c14f28
--- /dev/null
+++ b/src/web/templates/user/company_manage_post_types.html
@@ -0,0 +1,694 @@
+{% extends "company_base.html" %}
+{% block title %}Post-Typen - {{ employee_name }} - {{ session.company_name }}{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+
Post-Typen von {{ employee_name }}
+
Definiere und konfiguriere die Post-Kategorien
+
+
+
+
+
+
+ {% if not post_types_with_counts %}
+
+
+
Noch keine Post-Typen definiert
+
Erstelle den ersten Post-Typ für {{ employee_name }}.
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+ {% if post_types_with_counts %}
+
+
+
+ {% endif %}
+
+
+
+
+
+
Neuer Post-Typ
+
+
+
+
+
+
Wähle einen vorgefertigten Post-Typ oder erstelle einen eigenen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Post-Typ löschen?
+
Möchtest du diesen Post-Typ wirklich löschen?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/src/web/templates/user/company_manage_posts.html b/src/web/templates/user/company_manage_posts.html
index 397c3c0..a3dbc57 100644
--- a/src/web/templates/user/company_manage_posts.html
+++ b/src/web/templates/user/company_manage_posts.html
@@ -104,12 +104,14 @@
Posts von {{ employee_name }}
-
Ziehe Posts zwischen den Spalten um den Status zu ändern
+
Ziehe Posts zwischen den Spalten um den Status zu ändern. Die Spalte "Freigegeben" ist nur lesbar.
+ {% if not permissions or permissions.can_create_posts %}
Neuer Post
+ {% endif %}
@@ -155,20 +157,40 @@
-
-
+
+
-
+
{% for post in posts if post.status == 'ready' %}
- {{ render_post_card(post) }}
+
+
+
{{ post.topic_title or 'Untitled' }}
+ {% 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) %}
+ {{ score }}
+ {% endif %}
+
+
+
+
+ {{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}
+
+
+ {% if post.post_content %}
+
{{ post.post_content[:150] }}{% if post.post_content | length > 150 %}...{% endif %}
+ {% endif %}
+
{% else %}
-
+
Keine freigegebenen Posts
@@ -194,6 +216,22 @@
{% block scripts %}
+{% endblock %}
diff --git a/src/web/templates/user/post_detail.html b/src/web/templates/user/post_detail.html
index cc62b52..898b946 100644
--- a/src/web/templates/user/post_detail.html
+++ b/src/web/templates/user/post_detail.html
@@ -909,6 +909,37 @@
+
+
+
+
+
+ Planen
+
+ {% if post.status == 'scheduled' and post.scheduled_at %}
+
+
Geplant für
+
{{ post.scheduled_at.strftime('%d.%m.%Y %H:%M') }} Uhr
+
+
+ {% else %}
+ {% if post.status != 'ready' %}
+
Nur freigegebene Posts (Status "Freigegeben") können geplant werden.
+ {% endif %}
+
+
+ {% endif %}
+
@@ -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 = '
';
+
+ 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');
+ }
+}
{% endblock %}
diff --git a/src/web/templates/user/settings.html b/src/web/templates/user/settings.html
index f2a79da..f3d2fda 100644
--- a/src/web/templates/user/settings.html
+++ b/src/web/templates/user/settings.html
@@ -234,6 +234,103 @@
{% endif %}
+ {% if session and session.account_type == 'employee' and session.company_id and company_permissions %}
+
+
+
+
+ Unternehmens-Berechtigungen
+
+
+ Lege fest, was {{ company.name if company else 'dein Unternehmen' }} mit deinen Inhalten tun darf.
+ Standardmäßig sind alle Berechtigungen aktiviert.
+
+
+
+ {% endif %}
+
@@ -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 = '
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 = ' 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 {
diff --git a/src/web/user/routes.py b/src/web/user/routes.py
index 6ba66e3..f5fd7b7 100644
--- a/src/web/user/routes.py
+++ b/src/web/user/routes.py
@@ -27,7 +27,10 @@ from src.web.user.password_auth import (
)
from src.services.email_service import (
send_approval_request_email,
+ send_company_approval_request_email,
+ send_company_review_notification_email,
send_decision_notification_email,
+ generate_token,
validate_token,
mark_token_used,
)
@@ -95,6 +98,22 @@ async def get_user_avatar(session: UserSession, user_id: UUID) -> Optional[str]:
+async def get_employee_permissions_or_default(user_id: UUID, company_id: UUID) -> dict:
+ """Get employee permissions for a company, returning all-true defaults if no row exists."""
+ perms = await db.get_employee_permissions(user_id, company_id)
+ if perms:
+ return perms.model_dump()
+ return {
+ "can_create_posts": True,
+ "can_view_posts": True,
+ "can_edit_posts": True,
+ "can_schedule_posts": True,
+ "can_manage_post_types": True,
+ "can_do_research": True,
+ "can_see_in_calendar": True,
+ }
+
+
def require_user_session(request: Request) -> Optional[UserSession]:
"""Check if user is authenticated, redirect to login if not."""
session = get_user_session(request)
@@ -2133,8 +2152,16 @@ async def get_post_suggestions(request: Request, post_id: str):
if not post:
raise HTTPException(status_code=404, detail="Post not found")
- # Verify user owns this post
- if str(post.user_id) != session.user_id:
+ # Verify authorization: post owner or company with can_edit_posts
+ is_owner = str(post.user_id) == session.user_id
+ is_authorized = is_owner
+ if not is_owner and session.account_type == "company" and session.company_id:
+ profile_check = await db.get_profile(post.user_id)
+ if profile_check and profile_check.company_id and str(profile_check.company_id) == session.company_id:
+ edit_perms = await get_employee_permissions_or_default(post.user_id, UUID(session.company_id))
+ if edit_perms.get("can_edit_posts", True):
+ is_authorized = True
+ if not is_authorized:
raise HTTPException(status_code=403, detail="Not authorized")
# Get the last critic feedback if available
@@ -2178,8 +2205,16 @@ async def revise_post(
if not post:
raise HTTPException(status_code=404, detail="Post not found")
- # Verify user owns this post
- if str(post.user_id) != session.user_id:
+ # Verify authorization: post owner or company with can_edit_posts
+ is_owner = str(post.user_id) == session.user_id
+ is_authorized = is_owner
+ if not is_owner and session.account_type == "company" and session.company_id:
+ profile_check = await db.get_profile(post.user_id)
+ if profile_check and profile_check.company_id and str(profile_check.company_id) == session.company_id:
+ edit_perms = await get_employee_permissions_or_default(post.user_id, UUID(session.company_id))
+ if edit_perms.get("can_edit_posts", True):
+ is_authorized = True
+ if not is_authorized:
raise HTTPException(status_code=403, detail="Not authorized")
# Apply the suggestion
@@ -2248,25 +2283,66 @@ async def update_post_status(
if not is_owner and not is_company_owner:
raise HTTPException(status_code=403, detail="Not authorized")
+ # Company cannot set status to "ready" — only the employee/owner can
+ if status == "ready" and is_company_owner:
+ raise HTTPException(status_code=403, detail="Nur Mitarbeiter können Posts freigeben")
+
# Get profile for email settings
profile = await db.get_profile(post.user_id)
# Update status
await db.update_generated_post(UUID(post_id), {"status": status})
- # Send email when moving to "approved" (Bearbeitet)
+ base_url = str(request.base_url).rstrip('/')
email_sent = False
- if status == "approved" and profile and profile.customer_email:
- # Build base URL from request
- base_url = str(request.base_url).rstrip('/')
- email_sent = await send_approval_request_email(
- to_email=profile.customer_email,
- post_id=UUID(post_id),
- post_title=post.topic_title or "Untitled Post",
- post_content=post.post_content,
- base_url=base_url,
- image_url=post.image_url
- )
+
+ if status == "approved":
+ if is_company_owner:
+ # Company moved post to "approved": send approval request to employee (creator_email)
+ company = await db.get_company(UUID(session.company_id))
+ company_name = company.name if company else (session.company_name or "Ihr Unternehmen")
+
+ # Generate tokens once — shared by email and Telegram buttons
+ approve_token = await generate_token(UUID(post_id), "approve")
+ reject_token = await generate_token(UUID(post_id), "reject")
+ approve_url = f"{base_url}/api/email-action/{approve_token}"
+ reject_url = f"{base_url}/api/email-action/{reject_token}"
+
+ if profile and profile.creator_email:
+ email_sent = send_company_approval_request_email(
+ to_email=profile.creator_email,
+ post_title=post.topic_title or "Untitled Post",
+ post_content=post.post_content or "",
+ company_name=company_name,
+ approve_url=approve_url,
+ reject_url=reject_url,
+ image_url=post.image_url
+ )
+ # Send Telegram notification with inline approve/reject buttons
+ if settings.telegram_enabled:
+ try:
+ from src.services.telegram_service import telegram_service as _tg
+ telegram_account = await db.get_telegram_account(post.user_id)
+ if _tg and telegram_account and telegram_account.is_active:
+ await _tg.send_message(
+ chat_id=telegram_account.telegram_chat_id,
+ text=(
+ f"📋 {company_name} hat deinen Post bearbeitet und bittet um deine Freigabe:\n\n"
+ f"{post.topic_title or 'Untitled Post'}\n\n"
+ f"Bitte gib den Post frei oder lehne ihn ab:"
+ ),
+ reply_markup={
+ "inline_keyboard": [[
+ {"text": "✅ Freigeben", "url": approve_url},
+ {"text": "❌ Ablehnen", "url": reject_url}
+ ]]
+ }
+ )
+ except Exception as tg_err:
+ logger.warning(f"Telegram notification failed: {tg_err}")
+ else:
+ # Employee/owner moving their own post to "approved": no email/telegram notification
+ pass
return {"success": True, "status": status, "email_sent": email_sent}
except HTTPException:
@@ -2292,8 +2368,16 @@ async def update_post(
if not post:
raise HTTPException(status_code=404, detail="Post not found")
- # Verify user owns this post
- if str(post.user_id) != session.user_id:
+ # Verify user owns this post OR is a company owner with edit permission
+ is_owner = str(post.user_id) == session.user_id
+ is_authorized = is_owner
+ if not is_owner and session.account_type == "company" and session.company_id:
+ profile_check = await db.get_profile(post.user_id)
+ if profile_check and profile_check.company_id and str(profile_check.company_id) == session.company_id:
+ edit_perms = await get_employee_permissions_or_default(post.user_id, UUID(session.company_id))
+ if edit_perms.get("can_edit_posts", True):
+ is_authorized = True
+ if not is_authorized:
raise HTTPException(status_code=403, detail="Not authorized")
if len(content) > 3000:
@@ -2424,6 +2508,13 @@ async def settings_page(request: Request):
if settings.telegram_enabled:
telegram_account = await db.get_telegram_account(user_id)
+ # Get company permissions if user is an employee with a company
+ company_permissions = None
+ company = None
+ if session.account_type == "employee" and session.company_id:
+ company_permissions = await get_employee_permissions_or_default(user_id, UUID(session.company_id))
+ company = await db.get_company(UUID(session.company_id))
+
return templates.TemplateResponse("settings.html", {
"request": request,
"page": "settings",
@@ -2433,6 +2524,8 @@ async def settings_page(request: Request):
"linkedin_account": linkedin_account,
"telegram_enabled": settings.telegram_enabled,
"telegram_account": telegram_account,
+ "company_permissions": company_permissions,
+ "company": company,
})
except Exception as e:
logger.error(f"Error loading settings: {e}")
@@ -2471,6 +2564,38 @@ async def update_email_settings(
raise HTTPException(status_code=500, detail=str(e))
+@user_router.post("/api/settings/company-permissions")
+async def update_company_permissions(request: Request):
+ """Update employee-company permissions. Checkboxes present = True, absent = False."""
+ session = require_user_session(request)
+ if not session:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ if session.account_type != "employee" or not session.company_id:
+ raise HTTPException(status_code=403, detail="Only employees can update company permissions")
+
+ try:
+ form = await request.form()
+ updates = {
+ "can_create_posts": "can_create_posts" in form,
+ "can_view_posts": "can_view_posts" in form,
+ "can_edit_posts": "can_edit_posts" in form,
+ "can_schedule_posts": "can_schedule_posts" in form,
+ "can_manage_post_types": "can_manage_post_types" in form,
+ "can_do_research": "can_do_research" in form,
+ "can_see_in_calendar": "can_see_in_calendar" in form,
+ }
+ await db.upsert_employee_permissions(
+ UUID(session.user_id),
+ UUID(session.company_id),
+ updates
+ )
+ return {"success": True}
+ except Exception as e:
+ logger.exception(f"Failed to update company permissions: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
# ==================== LINKEDIN ACCOUNT LINKING ====================
@user_router.get("/settings/linkedin/connect")
@@ -2815,6 +2940,8 @@ async def company_manage_page(request: Request, employee_id: str = None):
pending_posts = 0
approved_posts = 0
+ selected_permissions = None
+
if employee_id:
# Find the selected employee
for emp in active_employees_info:
@@ -2830,6 +2957,7 @@ async def company_manage_page(request: Request, employee_id: str = None):
if emp_profile:
pending_posts = len([p for p in employee_posts if p.status in ['draft', 'pending']])
approved_posts = len([p for p in employee_posts if p.status in ['approved', 'published']])
+ selected_permissions = await get_employee_permissions_or_default(emp_profile.id, company_id)
user_id = UUID(session.user_id)
profile_picture = await get_user_avatar(session, user_id)
@@ -2843,6 +2971,7 @@ async def company_manage_page(request: Request, employee_id: str = None):
"employee_posts": employee_posts,
"pending_posts": pending_posts,
"approved_posts": approved_posts,
+ "selected_permissions": selected_permissions,
"profile_picture": profile_picture
})
@@ -2876,6 +3005,12 @@ async def company_manage_posts(request: Request, employee_id: str = None):
user_id = UUID(session.user_id)
profile_picture = await get_user_avatar(session, user_id)
+ permissions = await get_employee_permissions_or_default(emp_profile.id, UUID(session.company_id))
+
+ # Enforce can_view_posts permission
+ if not permissions.get("can_view_posts", True):
+ return RedirectResponse(url=f"/company/manage?employee_id={employee_id}&error=no_view_permission", status_code=302)
+
return templates.TemplateResponse("company_manage_posts.html", {
"request": request,
"page": "manage",
@@ -2885,7 +3020,9 @@ async def company_manage_posts(request: Request, employee_id: str = None):
"profile": profile,
"posts": posts,
"total_posts": len(posts),
- "profile_picture": profile_picture
+ "profile_picture": profile_picture,
+ "permissions": permissions,
+ "current_employee_id": employee_id,
})
@@ -2938,6 +3075,16 @@ async def company_manage_post_detail(request: Request, post_id: str, employee_id
for item in post.media_items
]
+ permissions = await get_employee_permissions_or_default(emp_profile.id, UUID(session.company_id))
+
+ # Check token limit for AI quick actions
+ limit_reached = False
+ limit_message = ""
+ if session.company_id:
+ can_proceed, error_msg, _, _ = await db.check_company_token_limit(UUID(session.company_id))
+ limit_reached = not can_proceed
+ limit_message = error_msg
+
return templates.TemplateResponse("company_manage_post_detail.html", {
"request": request,
"page": "manage",
@@ -2947,7 +3094,11 @@ async def company_manage_post_detail(request: Request, post_id: str, employee_id
"profile": profile,
"post": post,
"media_items_dict": media_items_dict,
- "profile_picture_url": profile_picture_url
+ "profile_picture_url": profile_picture_url,
+ "permissions": permissions,
+ "current_employee_id": employee_id,
+ "limit_reached": limit_reached,
+ "limit_message": limit_message,
})
@@ -3045,6 +3196,136 @@ async def company_manage_create(request: Request, employee_id: str = None):
})
+# ==================== COMPANY MANAGE CHAT-CREATE & POST TYPES ====================
+
+@user_router.get("/company/manage/chat-create", response_class=HTMLResponse)
+async def company_manage_chat_create(request: Request, employee_id: str = None):
+ """Chat-create page for a specific employee in company context."""
+ session = require_user_session(request)
+ if not session:
+ return RedirectResponse(url="/login", status_code=302)
+
+ if session.account_type != "company" or not session.company_id:
+ return RedirectResponse(url="/", status_code=302)
+
+ if not employee_id:
+ return RedirectResponse(url="/company/manage", status_code=302)
+
+ emp_profile = await db.get_profile(UUID(employee_id))
+ if not emp_profile:
+ return RedirectResponse(url="/company/manage", status_code=302)
+
+ emp_user = await db.get_user(UUID(employee_id))
+ if not emp_user or str(emp_user.company_id) != session.company_id:
+ return RedirectResponse(url="/company/manage", status_code=302)
+
+ # Check permission
+ permissions = await get_employee_permissions_or_default(emp_profile.id, UUID(session.company_id))
+ if not permissions.get("can_create_posts", True):
+ return RedirectResponse(url=f"/company/manage/posts?employee_id={employee_id}", status_code=302)
+
+ # Check token limit
+ limit_reached = False
+ limit_message = ""
+ can_create, error_msg, _, _ = await db.check_company_token_limit(UUID(session.company_id))
+ limit_reached = not can_create
+ limit_message = error_msg
+
+ # Get employee's post types
+ emp_post_types = await db.get_post_types(emp_profile.id)
+ if not emp_post_types:
+ emp_post_types = []
+
+ user_id = UUID(session.user_id)
+ profile_picture = await get_user_avatar(session, user_id)
+
+ # Load employee's saved posts for sidebar
+ all_posts = await db.get_generated_posts(emp_profile.id)
+ saved_posts = [post for post in all_posts if post.status not in ['scheduled', 'published']]
+
+ return templates.TemplateResponse("company_manage_chat_create.html", {
+ "request": request,
+ "page": "manage_chat_create",
+ "session": session,
+ "employee_id": employee_id,
+ "employee_name": emp_user.linkedin_name or emp_profile.display_name or emp_user.email,
+ "post_types": emp_post_types,
+ "profile_picture": profile_picture,
+ "saved_posts": saved_posts,
+ "limit_reached": limit_reached,
+ "limit_message": limit_message,
+ "permissions": permissions,
+ "current_employee_id": employee_id,
+ })
+
+
+@user_router.get("/company/manage/post-types", response_class=HTMLResponse)
+async def company_manage_post_types(request: Request, employee_id: str = None):
+ """Post types management page for a specific employee in company context."""
+ session = require_user_session(request)
+ if not session:
+ return RedirectResponse(url="/login", status_code=302)
+
+ if session.account_type != "company" or not session.company_id:
+ return RedirectResponse(url="/", status_code=302)
+
+ if not employee_id:
+ return RedirectResponse(url="/company/manage", status_code=302)
+
+ emp_profile = await db.get_profile(UUID(employee_id))
+ if not emp_profile:
+ return RedirectResponse(url="/company/manage", status_code=302)
+
+ emp_user = await db.get_user(UUID(employee_id))
+ if not emp_user or str(emp_user.company_id) != session.company_id:
+ return RedirectResponse(url="/company/manage", status_code=302)
+
+ # Check permission
+ permissions = await get_employee_permissions_or_default(emp_profile.id, UUID(session.company_id))
+ if not permissions.get("can_manage_post_types", True):
+ return RedirectResponse(url=f"/company/manage?employee_id={employee_id}", status_code=302)
+
+ user_id = UUID(session.user_id)
+ profile_picture = await get_user_avatar(session, user_id)
+
+ try:
+ import json
+ emp_id = emp_profile.id
+ post_types = await db.get_post_types(emp_id, active_only=True)
+ post_types_with_counts = []
+ for pt in post_types:
+ posts = await db.get_posts_by_type(emp_id, pt.id)
+ post_types_with_counts.append({
+ "post_type": {
+ "id": str(pt.id),
+ "name": pt.name,
+ "description": pt.description,
+ "strategy_weight": pt.strategy_weight,
+ "is_active": pt.is_active
+ },
+ "post_count": len(posts)
+ })
+ post_types_json = json.dumps(post_types_with_counts)
+ except Exception as e:
+ logger.error(f"Error loading company post types: {e}")
+ import json
+ post_types_with_counts = []
+ post_types_json = json.dumps([])
+
+ return templates.TemplateResponse("company_manage_post_types.html", {
+ "request": request,
+ "page": "manage_post_types",
+ "session": session,
+ "employee_id": employee_id,
+ "employee_name": emp_user.linkedin_name or emp_profile.display_name or emp_user.email,
+ "post_types_with_counts": post_types_with_counts,
+ "post_types_json": post_types_json,
+ "profile_picture": profile_picture,
+ "permissions": permissions,
+ "current_employee_id": employee_id,
+ })
+
+
# ==================== EMPLOYEE ROUTES ====================
@user_router.get("/employee/strategy", response_class=HTMLResponse)
@@ -3322,6 +3603,210 @@ async def delete_employee_post_type(request: Request, post_type_id: str, backgro
return JSONResponse({"error": str(e)}, status_code=500)
+
+# ============================================================================
+# COMPANY MANAGING EMPLOYEE POST TYPES
+# ============================================================================
+
+@user_router.post("/api/company/manage/post-types")
+async def company_create_employee_post_type(request: Request, background_tasks: BackgroundTasks):
+ """Company creates a post type for an employee."""
+ session = require_user_session(request)
+ if not session:
+ return JSONResponse({"error": "Not authenticated"}, status_code=401)
+ if session.account_type != "company" or not session.company_id:
+ return JSONResponse({"error": "Only company accounts can use this endpoint"}, status_code=403)
+
+ try:
+ data = await request.json()
+ employee_id_str = data.get("employee_id")
+ if not employee_id_str:
+ return JSONResponse({"error": "employee_id required"}, status_code=400)
+
+ emp_id = UUID(employee_id_str)
+ # Verify employee belongs to company
+ emp_user = await db.get_user(emp_id)
+ if not emp_user or str(emp_user.company_id) != session.company_id:
+ return JSONResponse({"error": "Employee not found or not in company"}, status_code=403)
+
+ perms = await get_employee_permissions_or_default(emp_id, UUID(session.company_id))
+ if not perms.get("can_manage_post_types", True):
+ return JSONResponse({"error": "Keine Berechtigung zum Verwalten von Post-Typen"}, status_code=403)
+
+ name_raw = data.get("name")
+ name = name_raw.strip() if name_raw else ""
+ if not name or len(name) < 3:
+ return JSONResponse({"error": "Name must be at least 3 characters"}, status_code=400)
+
+ description_raw = data.get("description")
+ description = description_raw.strip() if description_raw else ""
+ strategy_weight = float(data.get("strategy_weight", 0.5))
+ if not (0.0 <= strategy_weight <= 1.0):
+ return JSONResponse({"error": "Strategy weight must be between 0.0 and 1.0"}, status_code=400)
+
+ # Check for inactive post type with same name
+ all_post_types = await db.get_post_types(emp_id, active_only=False)
+ existing_inactive = next((pt for pt in all_post_types if pt.name.lower() == name.lower() and not pt.is_active), None)
+
+ if existing_inactive:
+ await db.update_post_type(existing_inactive.id, {
+ "is_active": True,
+ "description": description if description else existing_inactive.description,
+ "strategy_weight": strategy_weight
+ })
+ created_post_type = await db.get_post_type(existing_inactive.id)
+ else:
+ from src.database.models import PostType
+ post_type = PostType(
+ user_id=emp_id,
+ name=name,
+ description=description if description else None,
+ strategy_weight=strategy_weight,
+ is_active=True
+ )
+ created_post_type = await db.create_post_type(post_type)
+
+ return JSONResponse({
+ "success": True,
+ "post_type": {
+ "id": str(created_post_type.id),
+ "name": created_post_type.name,
+ "description": created_post_type.description,
+ "strategy_weight": created_post_type.strategy_weight
+ }
+ })
+ except Exception as e:
+ logger.error(f"Error creating employee post type (company): {e}")
+ return JSONResponse({"error": str(e)}, status_code=500)
+
+
+@user_router.put("/api/company/manage/post-types/{post_type_id}")
+async def company_update_employee_post_type(request: Request, post_type_id: str):
+ """Company updates a post type for an employee."""
+ session = require_user_session(request)
+ if not session:
+ return JSONResponse({"error": "Not authenticated"}, status_code=401)
+ if session.account_type != "company" or not session.company_id:
+ return JSONResponse({"error": "Only company accounts can use this endpoint"}, status_code=403)
+
+ try:
+ data = await request.json()
+ employee_id_str = data.get("employee_id")
+ if not employee_id_str:
+ return JSONResponse({"error": "employee_id required"}, status_code=400)
+
+ emp_id = UUID(employee_id_str)
+ emp_user = await db.get_user(emp_id)
+ if not emp_user or str(emp_user.company_id) != session.company_id:
+ return JSONResponse({"error": "Employee not found or not in company"}, status_code=403)
+
+ perms = await get_employee_permissions_or_default(emp_id, UUID(session.company_id))
+ if not perms.get("can_manage_post_types", True):
+ return JSONResponse({"error": "Keine Berechtigung"}, status_code=403)
+
+ pt_id = UUID(post_type_id)
+ post_type = await db.get_post_type(pt_id)
+ if not post_type or post_type.user_id != emp_id:
+ return JSONResponse({"error": "Post type not found or access denied"}, status_code=404)
+
+ updates = {}
+ if "name" in data:
+ name = data["name"].strip() if data["name"] else ""
+ if not name or len(name) < 3:
+ return JSONResponse({"error": "Name must be at least 3 characters"}, status_code=400)
+ updates["name"] = name
+ if "description" in data:
+ desc = data["description"]
+ updates["description"] = desc.strip() if desc else None
+ if "strategy_weight" in data:
+ sw = float(data["strategy_weight"])
+ if not (0.0 <= sw <= 1.0):
+ return JSONResponse({"error": "Strategy weight must be between 0.0 and 1.0"}, status_code=400)
+ updates["strategy_weight"] = sw
+
+ if updates:
+ await db.update_post_type(pt_id, updates)
+ return JSONResponse({"success": True})
+ except Exception as e:
+ logger.error(f"Error updating employee post type (company): {e}")
+ return JSONResponse({"error": str(e)}, status_code=500)
+
+
+@user_router.delete("/api/company/manage/post-types/{post_type_id}")
+async def company_delete_employee_post_type(request: Request, post_type_id: str, employee_id: str = None):
+ """Company soft-deletes a post type for an employee."""
+ session = require_user_session(request)
+ if not session:
+ return JSONResponse({"error": "Not authenticated"}, status_code=401)
+ if session.account_type != "company" or not session.company_id:
+ return JSONResponse({"error": "Only company accounts can use this endpoint"}, status_code=403)
+ if not employee_id:
+ return JSONResponse({"error": "employee_id required"}, status_code=400)
+
+ try:
+ emp_id = UUID(employee_id)
+ emp_user = await db.get_user(emp_id)
+ if not emp_user or str(emp_user.company_id) != session.company_id:
+ return JSONResponse({"error": "Employee not found or not in company"}, status_code=403)
+
+ perms = await get_employee_permissions_or_default(emp_id, UUID(session.company_id))
+ if not perms.get("can_manage_post_types", True):
+ return JSONResponse({"error": "Keine Berechtigung"}, status_code=403)
+
+ pt_id = UUID(post_type_id)
+ post_type = await db.get_post_type(pt_id)
+ if not post_type or post_type.user_id != emp_id:
+ return JSONResponse({"error": "Post type not found or access denied"}, status_code=404)
+
+ posts = await db.get_posts_by_type(emp_id, pt_id)
+ await db.update_post_type(pt_id, {"is_active": False})
+ return JSONResponse({"success": True, "affected_posts": len(posts)})
+ except Exception as e:
+ logger.error(f"Error deleting employee post type (company): {e}")
+ return JSONResponse({"error": str(e)}, status_code=500)
+
+
+@user_router.post("/api/company/manage/post-types/save-all")
+async def company_save_all_employee_post_types(request: Request, background_tasks: BackgroundTasks):
+ """Company triggers re-categorization for employee post types."""
+ session = require_user_session(request)
+ if not session:
+ return JSONResponse({"error": "Not authenticated"}, status_code=401)
+ if session.account_type != "company" or not session.company_id:
+ return JSONResponse({"error": "Only company accounts can use this endpoint"}, status_code=403)
+
+ try:
+ data = await request.json()
+ employee_id_str = data.get("employee_id")
+ has_structural_changes = data.get("has_structural_changes", False)
+
+ if not employee_id_str:
+ return JSONResponse({"error": "employee_id required"}, status_code=400)
+
+ emp_id = UUID(employee_id_str)
+ emp_user = await db.get_user(emp_id)
+ if not emp_user or str(emp_user.company_id) != session.company_id:
+ return JSONResponse({"error": "Employee not found or not in company"}, status_code=403)
+
+ if has_structural_changes:
+ categorization_job = await job_manager.create_job(
+ job_type=JobType.POST_CATEGORIZATION,
+ user_id=str(emp_id)
+ )
+ analysis_job = await job_manager.create_job(
+ job_type=JobType.POST_TYPE_ANALYSIS,
+ user_id=str(emp_id)
+ )
+ background_tasks.add_task(run_post_recategorization, emp_id, categorization_job.id)
+ background_tasks.add_task(run_post_type_analysis, emp_id, analysis_job.id)
+ return JSONResponse({"success": True, "recategorized": True})
+
+ return JSONResponse({"success": True, "recategorized": False})
+ except Exception as e:
+ logger.error(f"Error in company save-all post types: {e}")
+ return JSONResponse({"error": str(e)}, status_code=500)
+
+
@user_router.post("/api/employee/post-types/save-all")
async def save_all_and_reanalyze(request: Request, background_tasks: BackgroundTasks):
"""Save all changes and conditionally trigger re-categorization based on structural changes."""
@@ -3798,6 +4283,158 @@ async def update_chat_post(request: Request, post_id: str):
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+# ==================== COMPANY MANAGE CHAT API (PROXY FOR EMPLOYEE) ====================
+
+@user_router.post("/api/company/manage/chat/generate")
+async def company_chat_generate_post(request: Request):
+ """Generate a post for an employee via company context."""
+ session = require_user_session(request)
+ if not session:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ if session.account_type != "company" or not session.company_id:
+ raise HTTPException(status_code=403, detail="Company account required")
+
+ try:
+ data = await request.json()
+ employee_id = data.get("employee_id")
+ message = data.get("message", "").strip()
+ post_type_id = data.get("post_type_id")
+
+ if not employee_id or not message or not post_type_id:
+ return JSONResponse({"success": False, "error": "employee_id, message und post_type_id erforderlich"})
+
+ # Verify employee belongs to this company
+ emp_profile = await db.get_profile(UUID(employee_id))
+ emp_user = await db.get_user(UUID(employee_id))
+ if not emp_profile or not emp_user or str(emp_user.company_id) != session.company_id:
+ return JSONResponse({"success": False, "error": "Mitarbeiter nicht gefunden"}, status_code=404)
+
+ # Check permission
+ perms = await get_employee_permissions_or_default(emp_profile.id, UUID(session.company_id))
+ if not perms.get("can_create_posts", True):
+ return JSONResponse({"success": False, "error": "Keine Berechtigung"}, status_code=403)
+
+ # Check token limit
+ can_proceed, error_msg, _, _ = await db.check_company_token_limit(UUID(session.company_id))
+ if not can_proceed:
+ return JSONResponse({"success": False, "token_limit_exceeded": True, "error": error_msg})
+
+ user_id = emp_profile.id
+
+ post_type = await db.get_post_type(UUID(post_type_id))
+ if not post_type:
+ return JSONResponse({"success": False, "error": "Post-Typ nicht gefunden"})
+
+ profile_analysis = await db.get_profile_analysis(user_id)
+ if not profile_analysis:
+ return JSONResponse({"success": False, "error": "Profil-Analyse nicht gefunden. Bitte führe erst eine Profilanalyse durch."})
+
+ company = await db.get_company(UUID(session.company_id))
+ company_strategy = company.company_strategy if company else None
+
+ linkedin_posts = await db.get_posts_by_type(user_id, UUID(post_type_id))
+ if len(linkedin_posts) < 3:
+ linkedin_posts = await db.get_linkedin_posts(user_id)
+
+ example_post_texts = [
+ post.post_text for post in linkedin_posts
+ if post.post_text and len(post.post_text) > 100
+ ][:10]
+
+ from src.agents.writer import WriterAgent
+ import uuid
+ writer = WriterAgent()
+ writer.set_tracking_context(
+ operation='post_creation',
+ user_id=session.user_id,
+ company_id=session.company_id
+ )
+
+ topic = {"title": message[:100], "fact": message, "relevance": "Company-created content"}
+ post_content = await writer.process(
+ topic=topic,
+ profile_analysis=profile_analysis.full_analysis,
+ example_posts=example_post_texts,
+ post_type=post_type,
+ user_thoughts=message,
+ company_strategy=company_strategy,
+ strategy_weight=post_type.strategy_weight
+ )
+
+ return JSONResponse({
+ "success": True,
+ "post": post_content,
+ "conversation_id": str(uuid.uuid4()),
+ "explanation": "Hier ist der erste Entwurf basierend auf der Beschreibung:"
+ })
+
+ except Exception as e:
+ logger.error(f"Error generating company chat post: {e}")
+ return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+
+
+@user_router.post("/api/company/manage/chat/save")
+async def company_chat_save_post(request: Request):
+ """Save a chat-generated post for an employee."""
+ session = require_user_session(request)
+ if not session:
+ raise HTTPException(status_code=401, detail="Not authenticated")
+
+ if session.account_type != "company" or not session.company_id:
+ raise HTTPException(status_code=403, detail="Company account required")
+
+ try:
+ data = await request.json()
+ employee_id = data.get("employee_id")
+ post_content = data.get("post_content", "").strip()
+ post_type_id = data.get("post_type_id")
+ chat_history = data.get("chat_history", [])
+ topic_title = data.get("topic_title", post_content[:80] if post_content else "Chat Post")
+
+ if not employee_id or not post_content:
+ return JSONResponse({"success": False, "error": "employee_id und post_content erforderlich"})
+
+ emp_profile = await db.get_profile(UUID(employee_id))
+ emp_user = await db.get_user(UUID(employee_id))
+ if not emp_profile or not emp_user or str(emp_user.company_id) != session.company_id:
+ return JSONResponse({"success": False, "error": "Mitarbeiter nicht gefunden"}, status_code=404)
+
+ perms = await get_employee_permissions_or_default(emp_profile.id, UUID(session.company_id))
+ if not perms.get("can_create_posts", True):
+ return JSONResponse({"success": False, "error": "Keine Berechtigung"}, status_code=403)
+
+ writer_versions = [item['ai'] for item in chat_history if 'ai' in item and item['ai']]
+ critic_feedback_list = []
+ for item in chat_history:
+ if 'user' in item and item['user']:
+ critic_feedback_list.append({'feedback': item['user'], 'explanation': item.get('explanation', '')})
+
+ from src.database.models import GeneratedPost as GenPost
+ new_post = GenPost(
+ user_id=emp_profile.id,
+ topic_title=topic_title,
+ post_content=post_content,
+ writer_versions=writer_versions if writer_versions else [post_content],
+ critic_feedback=critic_feedback_list,
+ iterations=len(writer_versions) if writer_versions else 1,
+ status="draft",
+ post_type_id=UUID(post_type_id) if post_type_id else None
+ )
+
+ saved_post = await db.save_generated_post(new_post)
+
+ return JSONResponse({
+ "success": True,
+ "post_id": str(saved_post.id),
+ "message": "Post erfolgreich gespeichert"
+ })
+
+ except Exception as e:
+ logger.error(f"Error saving company chat post: {e}")
+ return JSONResponse({"success": False, "error": str(e)}, status_code=500)
+
+
@user_router.post("/api/company/invite")
async def send_company_invitation(request: Request):
"""Send invitation to a new employee."""
@@ -4148,7 +4785,29 @@ async def company_calendar_page(request: Request, month: int = None, year: int =
month_name = month_names[current_month - 1]
# Get all posts for the company (already enriched with employee info - single optimized query)
- all_posts = await db.get_scheduled_posts_for_company(company_id)
+ all_posts_raw = await db.get_scheduled_posts_for_company(company_id)
+
+ # Filter out employees who have disabled calendar visibility
+ all_posts = []
+ hidden_user_ids = set()
+ can_schedule_user_ids = set()
+ fetched_perms_uids = set()
+ for post in all_posts_raw:
+ uid = post.get("user_id") or post.get("employee_user_id")
+ if uid and uid not in fetched_perms_uids and uid not in hidden_user_ids:
+ fetched_perms_uids.add(uid)
+ try:
+ perms = await get_employee_permissions_or_default(UUID(uid), company_id)
+ if not perms.get("can_see_in_calendar", True):
+ hidden_user_ids.add(uid)
+ continue
+ if perms.get("can_schedule_posts", True):
+ can_schedule_user_ids.add(uid)
+ except Exception:
+ can_schedule_user_ids.add(uid) # default allow
+ if uid in hidden_user_ids:
+ continue
+ all_posts.append(post)
# Build employee list from posts (no extra queries needed)
employee_map = {}
@@ -4158,7 +4817,6 @@ async def company_calendar_page(request: Request, month: int = None, year: int =
employee_map[user_id] = {
"user_id": user_id,
"name": post.get("employee_name", "Unbekannt"),
- "user_id": post.get("employee_user_id")
}
employee_customers = list(employee_map.values())
@@ -4207,6 +4865,8 @@ async def company_calendar_page(request: Request, month: int = None, year: int =
posts_by_date[date_key].append(post_data)
elif post.get("status") == "ready":
# Unscheduled but ready for scheduling
+ uid = str(post.get("user_id", ""))
+ post_data["can_schedule"] = uid in can_schedule_user_ids
unscheduled_posts.append(post_data)
# Build calendar weeks
@@ -4299,6 +4959,144 @@ async def company_calendar_page(request: Request, month: int = None, year: int =
})
+@user_router.get("/calendar", response_class=HTMLResponse)
+async def employee_calendar_page(request: Request, month: int = None, year: int = None, view: str = "month", week_start: str = None):
+ """Employee/ghostwriter personal posting calendar page."""
+ from datetime import date, datetime, timedelta
+ import calendar
+
+ session = require_user_session(request)
+ if not session:
+ return RedirectResponse(url="/login", status_code=302)
+
+ if session.account_type == "company":
+ return RedirectResponse(url="/company/calendar", status_code=302)
+
+ user_id = UUID(session.user_id)
+ today = date.today()
+ current_month = month or today.month
+ current_year = year or today.year
+
+ if current_month == 1:
+ prev_month, prev_year = 12, current_year - 1
+ else:
+ prev_month, prev_year = current_month - 1, current_year
+
+ if current_month == 12:
+ next_month, next_year = 1, current_year + 1
+ else:
+ next_month, next_year = current_month + 1, current_year
+
+ month_names = ["Januar", "Februar", "März", "April", "Mai", "Juni",
+ "Juli", "August", "September", "Oktober", "November", "Dezember"]
+ month_name = month_names[current_month - 1]
+
+ # Get all own posts that are in relevant statuses
+ all_posts = await db.get_scheduled_posts_for_user(user_id)
+
+ # Build posts by date
+ posts_by_date = {}
+ unscheduled_posts = []
+
+ for post in all_posts:
+ post_data = {
+ "id": str(post.id),
+ "topic_title": post.topic_title or "Ohne Titel",
+ "post_content": post.post_content,
+ "status": post.status,
+ }
+
+ if post.scheduled_at and post.status in ("scheduled", "published"):
+ scheduled_dt = post.scheduled_at
+ if not scheduled_dt.tzinfo:
+ scheduled_dt = scheduled_dt.replace(tzinfo=timezone.utc)
+ date_key = scheduled_dt.strftime("%Y-%m-%d")
+ post_data["time"] = scheduled_dt.strftime("%H:%M")
+ if date_key not in posts_by_date:
+ posts_by_date[date_key] = []
+ posts_by_date[date_key].append(post_data)
+ elif post.status in ("ready", "approved"):
+ unscheduled_posts.append(post_data)
+
+ cal = calendar.Calendar(firstweekday=0)
+ month_days = cal.monthdayscalendar(current_year, current_month)
+ calendar_weeks = []
+ week_label = None
+ prev_ws = None
+ next_ws = None
+
+ if view == "week":
+ if week_start:
+ try:
+ ws_date = date.fromisoformat(week_start)
+ except ValueError:
+ ws_date = today - timedelta(days=today.weekday())
+ else:
+ ws_date = today - timedelta(days=today.weekday())
+
+ week_data = []
+ for i in range(7):
+ d = ws_date + timedelta(days=i)
+ date_str = d.isoformat()
+ week_data.append({
+ "day": d.day,
+ "date": date_str,
+ "other_month": d.month != current_month,
+ "is_today": d == today,
+ "posts": posts_by_date.get(date_str, []),
+ "weekday_name": ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"][i],
+ "full_date": f"{d.day}. {month_names[d.month - 1]}"
+ })
+ calendar_weeks.append(week_data)
+
+ prev_ws = ws_date - timedelta(weeks=1)
+ next_ws = ws_date + timedelta(weeks=1)
+ ws_end = ws_date + timedelta(days=6)
+
+ if ws_date.month == ws_end.month:
+ week_label = f"{ws_date.day}. - {ws_end.day}. {month_names[ws_date.month - 1]} {ws_date.year}"
+ else:
+ week_label = f"{ws_date.day}. {month_names[ws_date.month - 1]} - {ws_end.day}. {month_names[ws_end.month - 1]} {ws_end.year}"
+ else:
+ for week in month_days:
+ week_data = []
+ for day in week:
+ if day == 0:
+ week_data.append({"day": "", "date": "", "other_month": True, "is_today": False, "posts": []})
+ else:
+ date_str = f"{current_year}-{current_month:02d}-{day:02d}"
+ is_today = (day == today.day and current_month == today.month and current_year == today.year)
+ week_data.append({
+ "day": day, "date": date_str, "other_month": False,
+ "is_today": is_today, "posts": posts_by_date.get(date_str, [])
+ })
+ calendar_weeks.append(week_data)
+
+ avatar_url = await get_user_avatar(session, user_id)
+
+ return templates.TemplateResponse("employee_calendar.html", {
+ "request": request,
+ "page": "calendar",
+ "session": session,
+ "avatar_url": avatar_url,
+ "month": current_month,
+ "year": current_year,
+ "month_name": month_name,
+ "prev_month": prev_month,
+ "prev_year": prev_year,
+ "next_month": next_month,
+ "next_year": next_year,
+ "current_month": today.month,
+ "current_year": today.year,
+ "calendar_weeks": calendar_weeks,
+ "unscheduled_posts": unscheduled_posts,
+ "view": view,
+ "week_label": week_label,
+ "prev_week_start": prev_ws.isoformat() if prev_ws else None,
+ "next_week_start": next_ws.isoformat() if next_ws else None,
+ })
+
+
@user_router.get("/api/posts/{post_id}")
async def get_post_api(request: Request, post_id: str):
"""Get a single post as JSON."""
@@ -4347,19 +5145,28 @@ async def schedule_post(request: Request, post_id: str, scheduled_at: str = Form
if not session:
raise HTTPException(status_code=401, detail="Not authenticated")
- # Only company owners can schedule for now
- if session.account_type != "company" or not session.company_id:
- raise HTTPException(status_code=403, detail="Only company owners can schedule posts")
-
try:
post = await db.get_generated_post(UUID(post_id))
if not post:
raise HTTPException(status_code=404, detail="Post not found")
- # Verify this post belongs to a company employee
- profile = await db.get_profile(post.user_id)
- if not profile or not profile.company_id or str(profile.company_id) != session.company_id:
- raise HTTPException(status_code=403, detail="Not authorized")
+ # Determine authorization
+ is_post_owner = str(post.user_id) == session.user_id
+ is_company_scheduling = session.account_type == "company" and session.company_id
+
+ if is_post_owner:
+ # Post owner can always schedule their own posts
+ pass
+ elif is_company_scheduling:
+ # Company must have can_schedule_posts permission
+ profile = await db.get_profile(post.user_id)
+ if not profile or not profile.company_id or str(profile.company_id) != session.company_id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ perms = await get_employee_permissions_or_default(post.user_id, UUID(session.company_id))
+ if not perms.get("can_schedule_posts", True):
+ raise HTTPException(status_code=403, detail="Keine Berechtigung zum Einplanen von Posts")
+ else:
+ raise HTTPException(status_code=403, detail="Not authorized to schedule posts")
# Only ready posts can be scheduled
if post.status not in ["ready", "scheduled"]:
@@ -4397,19 +5204,27 @@ async def unschedule_post(request: Request, post_id: str):
if not session:
raise HTTPException(status_code=401, detail="Not authenticated")
- # Only company owners can unschedule for now
- if session.account_type != "company" or not session.company_id:
- raise HTTPException(status_code=403, detail="Only company owners can unschedule posts")
-
try:
post = await db.get_generated_post(UUID(post_id))
if not post:
raise HTTPException(status_code=404, detail="Post not found")
- # Verify this post belongs to a company employee
- profile = await db.get_profile(post.user_id)
- if not profile or not profile.company_id or str(profile.company_id) != session.company_id:
- raise HTTPException(status_code=403, detail="Not authorized")
+ # Determine authorization
+ is_post_owner = str(post.user_id) == session.user_id
+ is_company_scheduling = session.account_type == "company" and session.company_id
+
+ if is_post_owner:
+ # Post owner can always unschedule their own posts
+ pass
+ elif is_company_scheduling:
+ profile = await db.get_profile(post.user_id)
+ if not profile or not profile.company_id or str(profile.company_id) != session.company_id:
+ raise HTTPException(status_code=403, detail="Not authorized")
+ perms = await get_employee_permissions_or_default(post.user_id, UUID(session.company_id))
+ if not perms.get("can_schedule_posts", True):
+ raise HTTPException(status_code=403, detail="Keine Berechtigung zum Einplanen von Posts")
+ else:
+ raise HTTPException(status_code=403, detail="Not authorized to unschedule posts")
# Unschedule the post
updated_post = await db.unschedule_post(UUID(post_id))