Post Typen Verwalten + Strategy weight
This commit is contained in:
10
config/migrate_add_strategy_weight.sql
Normal file
10
config/migrate_add_strategy_weight.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- Add strategy_weight column to post_types table
|
||||||
|
-- This column controls how strongly the company strategy influences AI post generation
|
||||||
|
-- Range: 0.0 (ignore strategy) to 1.0 (strictly follow strategy)
|
||||||
|
|
||||||
|
ALTER TABLE post_types
|
||||||
|
ADD COLUMN IF NOT EXISTS strategy_weight FLOAT DEFAULT 0.5
|
||||||
|
CHECK (strategy_weight >= 0.0 AND strategy_weight <= 1.0);
|
||||||
|
|
||||||
|
-- Set default value for existing rows
|
||||||
|
UPDATE post_types SET strategy_weight = 0.5 WHERE strategy_weight IS NULL;
|
||||||
@@ -184,8 +184,10 @@ class PostClassifierAgent(BaseAgent):
|
|||||||
|
|
||||||
system_prompt = """Du bist ein Content-Analyst, der LinkedIn-Posts in vordefinierte Kategorien einordnet.
|
system_prompt = """Du bist ein Content-Analyst, der LinkedIn-Posts in vordefinierte Kategorien einordnet.
|
||||||
|
|
||||||
|
WICHTIG: JEDER Post MUSS kategorisiert werden. Du MUSST für jeden Post den am besten passenden Post-Typ wählen, auch wenn die Übereinstimmung nicht perfekt ist.
|
||||||
|
|
||||||
Analysiere jeden Post und ordne ihn dem passendsten Post-Typ zu.
|
Analysiere jeden Post und ordne ihn dem passendsten Post-Typ zu.
|
||||||
Wenn kein Typ wirklich passt, gib "null" als post_type_id zurück.
|
Es ist NICHT erlaubt, "null" als post_type_id zurückzugeben.
|
||||||
|
|
||||||
Bewerte die Zuordnung mit einer Confidence zwischen 0.3 und 1.0:
|
Bewerte die Zuordnung mit einer Confidence zwischen 0.3 und 1.0:
|
||||||
- 0.9-1.0: Sehr sicher, Post passt perfekt zum Typ
|
- 0.9-1.0: Sehr sicher, Post passt perfekt zum Typ
|
||||||
@@ -209,12 +211,14 @@ Gib ein JSON-Objekt zurück mit diesem Format:
|
|||||||
"classifications": [
|
"classifications": [
|
||||||
{{
|
{{
|
||||||
"post_id": "uuid-des-posts",
|
"post_id": "uuid-des-posts",
|
||||||
"post_type_id": "uuid-des-typs oder null",
|
"post_type_id": "uuid-des-typs (IMMER erforderlich, niemals null)",
|
||||||
"confidence": 0.8,
|
"confidence": 0.8,
|
||||||
"reasoning": "Kurze Begründung"
|
"reasoning": "Kurze Begründung"
|
||||||
}}
|
}}
|
||||||
]
|
]
|
||||||
}}"""
|
}}
|
||||||
|
|
||||||
|
WICHTIG: Jeder Post MUSS einen post_type_id bekommen. Wähle den am besten passenden Typ, auch wenn die Übereinstimmung nicht perfekt ist."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self.call_openai(
|
response = await self.call_openai(
|
||||||
@@ -230,6 +234,8 @@ Gib ein JSON-Objekt zurück mit diesem Format:
|
|||||||
|
|
||||||
# Process and validate results
|
# Process and validate results
|
||||||
valid_results = []
|
valid_results = []
|
||||||
|
unclassified_posts = list(posts) # Track which posts still need classification
|
||||||
|
|
||||||
for c in classifications:
|
for c in classifications:
|
||||||
post_id = c.get("post_id")
|
post_id = c.get("post_id")
|
||||||
post_type_id = c.get("post_type_id")
|
post_type_id = c.get("post_type_id")
|
||||||
@@ -244,7 +250,8 @@ Gib ein JSON-Objekt zurück mit diesem Format:
|
|||||||
# Validate post_type_id
|
# Validate post_type_id
|
||||||
if post_type_id and post_type_id != "null" and post_type_id not in valid_type_ids:
|
if post_type_id and post_type_id != "null" and post_type_id not in valid_type_ids:
|
||||||
logger.warning(f"Invalid post_type_id in classification: {post_type_id}")
|
logger.warning(f"Invalid post_type_id in classification: {post_type_id}")
|
||||||
continue
|
# Fallback: assign to first available type
|
||||||
|
post_type_id = list(valid_type_ids - {"null"})[0] if valid_type_ids - {"null"} else None
|
||||||
|
|
||||||
if post_type_id and post_type_id != "null":
|
if post_type_id and post_type_id != "null":
|
||||||
valid_results.append({
|
valid_results.append({
|
||||||
@@ -253,6 +260,19 @@ Gib ein JSON-Objekt zurück mit diesem Format:
|
|||||||
"classification_method": "semantic",
|
"classification_method": "semantic",
|
||||||
"classification_confidence": min(1.0, max(0.3, confidence))
|
"classification_confidence": min(1.0, max(0.3, confidence))
|
||||||
})
|
})
|
||||||
|
unclassified_posts = [p for p in unclassified_posts if p.id != matching_post.id]
|
||||||
|
|
||||||
|
# FORCE CLASSIFY remaining unclassified posts
|
||||||
|
if unclassified_posts and post_types:
|
||||||
|
default_type = post_types[0] # Use first type as fallback
|
||||||
|
logger.warning(f"Force-classifying {len(unclassified_posts)} posts to default type: {default_type.name}")
|
||||||
|
for post in unclassified_posts:
|
||||||
|
valid_results.append({
|
||||||
|
"post_id": post.id,
|
||||||
|
"post_type_id": default_type.id,
|
||||||
|
"classification_method": "semantic_fallback",
|
||||||
|
"classification_confidence": 0.3
|
||||||
|
})
|
||||||
|
|
||||||
return valid_results
|
return valid_results
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ class ResearchAgent(BaseAgent):
|
|||||||
customer_data: Dict[str, Any],
|
customer_data: Dict[str, Any],
|
||||||
example_posts: List[str] = None,
|
example_posts: List[str] = None,
|
||||||
post_type: Any = None,
|
post_type: Any = None,
|
||||||
post_type_analysis: Dict[str, Any] = None
|
post_type_analysis: Dict[str, Any] = None,
|
||||||
|
company_strategy: Dict[str, Any] = None,
|
||||||
|
strategy_weight: float = 0.5
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Research new content topics.
|
Research new content topics.
|
||||||
@@ -34,6 +36,8 @@ class ResearchAgent(BaseAgent):
|
|||||||
example_posts: List of the person's actual posts for style reference
|
example_posts: List of the person's actual posts for style reference
|
||||||
post_type: Optional PostType object for targeted research
|
post_type: Optional PostType object for targeted research
|
||||||
post_type_analysis: Optional post type analysis for context
|
post_type_analysis: Optional post type analysis for context
|
||||||
|
company_strategy: Optional company strategy to align topics with
|
||||||
|
strategy_weight: Strategy weight (0.0-1.0) controlling strategy influence
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Research results with suggested topics
|
Research results with suggested topics
|
||||||
@@ -90,7 +94,9 @@ class ResearchAgent(BaseAgent):
|
|||||||
example_posts=example_posts or [],
|
example_posts=example_posts or [],
|
||||||
existing_topics=existing_topics,
|
existing_topics=existing_topics,
|
||||||
post_type=post_type,
|
post_type=post_type,
|
||||||
post_type_analysis=post_type_analysis
|
post_type_analysis=post_type_analysis,
|
||||||
|
company_strategy=company_strategy,
|
||||||
|
strategy_weight=strategy_weight
|
||||||
)
|
)
|
||||||
|
|
||||||
response = await self.call_openai(
|
response = await self.call_openai(
|
||||||
@@ -379,7 +385,9 @@ QUALITÄTSKRITERIEN:
|
|||||||
example_posts: List[str],
|
example_posts: List[str],
|
||||||
existing_topics: List[str],
|
existing_topics: List[str],
|
||||||
post_type: Any = None,
|
post_type: Any = None,
|
||||||
post_type_analysis: Dict[str, Any] = None
|
post_type_analysis: Dict[str, Any] = None,
|
||||||
|
company_strategy: Dict[str, Any] = None,
|
||||||
|
strategy_weight: float = 0.5
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Transform raw research into personalized, concrete topic suggestions."""
|
"""Transform raw research into personalized, concrete topic suggestions."""
|
||||||
|
|
||||||
@@ -428,8 +436,11 @@ QUALITÄTSKRITERIEN:
|
|||||||
|
|
||||||
post_type_section += "\n**WICHTIG:** Alle Themenvorschläge müssen zu diesem Post-Typ passen!\n"
|
post_type_section += "\n**WICHTIG:** Alle Themenvorschläge müssen zu diesem Post-Typ passen!\n"
|
||||||
|
|
||||||
|
# Build company strategy section
|
||||||
|
strategy_section = self._get_strategy_section(company_strategy, strategy_weight)
|
||||||
|
|
||||||
return f"""AUFGABE: Transformiere die Recherche-Ergebnisse in KONKRETE, PERSONALISIERTE Themenvorschläge.
|
return f"""AUFGABE: Transformiere die Recherche-Ergebnisse in KONKRETE, PERSONALISIERTE Themenvorschläge.
|
||||||
{post_type_section}
|
{post_type_section}{strategy_section}
|
||||||
|
|
||||||
=== RECHERCHE-ERGEBNISSE (Rohdaten) ===
|
=== RECHERCHE-ERGEBNISSE (Rohdaten) ===
|
||||||
{raw_research}
|
{raw_research}
|
||||||
@@ -492,6 +503,71 @@ WICHTIG:
|
|||||||
- Hook-Ideen müssen zum Stil der Beispiel-Posts passen!
|
- Hook-Ideen müssen zum Stil der Beispiel-Posts passen!
|
||||||
- Key Facts müssen aus der Recherche stammen (keine erfundenen Zahlen)"""
|
- Key Facts müssen aus der Recherche stammen (keine erfundenen Zahlen)"""
|
||||||
|
|
||||||
|
def _get_strategy_section(self, company_strategy: Dict[str, Any] = None, strategy_weight: float = 0.5) -> str:
|
||||||
|
"""Build company strategy section for research prompt with weighted influence."""
|
||||||
|
if not company_strategy:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Extract strategy elements
|
||||||
|
mission = company_strategy.get("mission", "")
|
||||||
|
vision = company_strategy.get("vision", "")
|
||||||
|
content_pillars = company_strategy.get("content_pillars", [])
|
||||||
|
target_audience = company_strategy.get("target_audience", "")
|
||||||
|
dos = company_strategy.get("dos", [])
|
||||||
|
donts = company_strategy.get("donts", [])
|
||||||
|
|
||||||
|
# Build section only if there's meaningful content
|
||||||
|
if not any([mission, vision, content_pillars, dos, donts]):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Determine importance level based on strategy_weight
|
||||||
|
if strategy_weight >= 0.8:
|
||||||
|
importance = "KRITISCH - STRIKT BEACHTEN"
|
||||||
|
instruction = "Alle Themen MÜSSEN sich an der Unternehmensstrategie orientieren! Nur Topics vorschlagen, die perfekt zur Strategie passen."
|
||||||
|
elif strategy_weight >= 0.6:
|
||||||
|
importance = "WICHTIG - BERÜCKSICHTIGEN"
|
||||||
|
instruction = "Priorisiere Topics die zur Unternehmensstrategie passen! Die Strategie soll klar erkennbar sein."
|
||||||
|
elif strategy_weight >= 0.4:
|
||||||
|
importance = "ORIENTIERUNG"
|
||||||
|
instruction = "Berücksichtige die Unternehmensstrategie wo sinnvoll - persönliche Expertise hat Priorität."
|
||||||
|
elif strategy_weight >= 0.2:
|
||||||
|
importance = "OPTIONAL"
|
||||||
|
instruction = "Die Strategie dient als Hintergrundinformation - freie Themenauswahl erlaubt."
|
||||||
|
else:
|
||||||
|
importance = "NUR REFERENZ"
|
||||||
|
instruction = "Ignoriere die Strategie weitgehend - Fokus auf persönliche Themen und Expertise."
|
||||||
|
|
||||||
|
section = f"""
|
||||||
|
|
||||||
|
=== UNTERNEHMENSSTRATEGIE ({importance}) ===
|
||||||
|
Strategy Weight: {strategy_weight:.1f} / 1.0
|
||||||
|
{instruction}
|
||||||
|
"""
|
||||||
|
if mission:
|
||||||
|
section += f"\nMISSION: {mission}"
|
||||||
|
if vision:
|
||||||
|
section += f"\nVISION: {vision}"
|
||||||
|
if content_pillars:
|
||||||
|
section += f"\nCONTENT PILLARS: {', '.join(content_pillars)}"
|
||||||
|
if target_audience:
|
||||||
|
section += f"\nZIELGRUPPE: {target_audience}"
|
||||||
|
if dos:
|
||||||
|
section += f"\n\nEMPFOHLENE THEMEN-RICHTUNGEN:\n" + "\n".join([f" + {d}" for d in dos])
|
||||||
|
if donts:
|
||||||
|
section += f"\n\nZU VERMEIDENDE THEMEN:\n" + "\n".join([f" - {d}" for d in donts])
|
||||||
|
|
||||||
|
# Weight-specific closing instruction
|
||||||
|
if strategy_weight >= 0.8:
|
||||||
|
section += "\n\n**KRITISCH:** Alle Topics müssen zur Unternehmensstrategie passen - keine Ausnahmen!"
|
||||||
|
elif strategy_weight >= 0.6:
|
||||||
|
section += "\n\n**WICHTIG:** Stelle sicher, dass die Mehrheit der Topics die Unternehmenswerte widerspiegelt!"
|
||||||
|
elif strategy_weight >= 0.4:
|
||||||
|
section += "\n\n**BEACHTE:** Integriere die Strategie subtil - persönliche Expertise bleibt wichtig!"
|
||||||
|
else:
|
||||||
|
section += "\n\n**REFERENZ:** Die Strategie dient nur als Orientierung - persönliche Topics haben Vorrang!"
|
||||||
|
|
||||||
|
return section + "\n"
|
||||||
|
|
||||||
def _get_structure_prompt(
|
def _get_structure_prompt(
|
||||||
self,
|
self,
|
||||||
raw_research: str,
|
raw_research: str,
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ class WriterAgent(BaseAgent):
|
|||||||
post_type_analysis: Optional[Dict[str, Any]] = None,
|
post_type_analysis: Optional[Dict[str, Any]] = None,
|
||||||
user_thoughts: str = "",
|
user_thoughts: str = "",
|
||||||
selected_hook: str = "",
|
selected_hook: str = "",
|
||||||
company_strategy: Optional[Dict[str, Any]] = None
|
company_strategy: Optional[Dict[str, Any]] = None,
|
||||||
|
strategy_weight: float = 0.5
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Write a LinkedIn post.
|
Write a LinkedIn post.
|
||||||
@@ -46,6 +47,7 @@ class WriterAgent(BaseAgent):
|
|||||||
post_type: Optional PostType object for type-specific writing
|
post_type: Optional PostType object for type-specific writing
|
||||||
post_type_analysis: Optional analysis of the post type
|
post_type_analysis: Optional analysis of the post type
|
||||||
company_strategy: Optional company strategy to consider during writing
|
company_strategy: Optional company strategy to consider during writing
|
||||||
|
strategy_weight: Strategy weight (0.0-1.0) controlling how strongly the company strategy influences post generation
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Written LinkedIn post
|
Written LinkedIn post
|
||||||
@@ -65,7 +67,8 @@ class WriterAgent(BaseAgent):
|
|||||||
post_type_analysis=post_type_analysis,
|
post_type_analysis=post_type_analysis,
|
||||||
user_thoughts=user_thoughts,
|
user_thoughts=user_thoughts,
|
||||||
selected_hook=selected_hook,
|
selected_hook=selected_hook,
|
||||||
company_strategy=company_strategy
|
company_strategy=company_strategy,
|
||||||
|
strategy_weight=strategy_weight
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info(f"Writing initial post for topic: {topic.get('title', 'Unknown')}")
|
logger.info(f"Writing initial post for topic: {topic.get('title', 'Unknown')}")
|
||||||
@@ -90,7 +93,8 @@ class WriterAgent(BaseAgent):
|
|||||||
post_type_analysis=post_type_analysis,
|
post_type_analysis=post_type_analysis,
|
||||||
user_thoughts=user_thoughts,
|
user_thoughts=user_thoughts,
|
||||||
selected_hook=selected_hook,
|
selected_hook=selected_hook,
|
||||||
company_strategy=company_strategy
|
company_strategy=company_strategy,
|
||||||
|
strategy_weight=strategy_weight
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return await self._write_single_draft(
|
return await self._write_single_draft(
|
||||||
@@ -102,7 +106,8 @@ class WriterAgent(BaseAgent):
|
|||||||
post_type_analysis=post_type_analysis,
|
post_type_analysis=post_type_analysis,
|
||||||
user_thoughts=user_thoughts,
|
user_thoughts=user_thoughts,
|
||||||
selected_hook=selected_hook,
|
selected_hook=selected_hook,
|
||||||
company_strategy=company_strategy
|
company_strategy=company_strategy,
|
||||||
|
strategy_weight=strategy_weight
|
||||||
)
|
)
|
||||||
|
|
||||||
def _select_example_posts(
|
def _select_example_posts(
|
||||||
@@ -236,7 +241,8 @@ class WriterAgent(BaseAgent):
|
|||||||
post_type_analysis: Optional[Dict[str, Any]] = None,
|
post_type_analysis: Optional[Dict[str, Any]] = None,
|
||||||
user_thoughts: str = "",
|
user_thoughts: str = "",
|
||||||
selected_hook: str = "",
|
selected_hook: str = "",
|
||||||
company_strategy: Optional[Dict[str, Any]] = None
|
company_strategy: Optional[Dict[str, Any]] = None,
|
||||||
|
strategy_weight: float = 0.5
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate multiple drafts and select the best one.
|
Generate multiple drafts and select the best one.
|
||||||
@@ -249,6 +255,7 @@ class WriterAgent(BaseAgent):
|
|||||||
post_type: Optional PostType object
|
post_type: Optional PostType object
|
||||||
post_type_analysis: Optional post type analysis
|
post_type_analysis: Optional post type analysis
|
||||||
company_strategy: Optional company strategy to consider
|
company_strategy: Optional company strategy to consider
|
||||||
|
strategy_weight: Strategy weight (0.0-1.0) for company strategy influence
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Best selected draft
|
Best selected draft
|
||||||
@@ -256,7 +263,7 @@ class WriterAgent(BaseAgent):
|
|||||||
num_drafts = min(max(settings.writer_multi_draft_count, 2), 5) # Clamp between 2-5
|
num_drafts = min(max(settings.writer_multi_draft_count, 2), 5) # Clamp between 2-5
|
||||||
logger.info(f"Generating {num_drafts} drafts for selection")
|
logger.info(f"Generating {num_drafts} drafts for selection")
|
||||||
|
|
||||||
system_prompt = self._get_system_prompt(profile_analysis, example_posts, learned_lessons, post_type, post_type_analysis, company_strategy)
|
system_prompt = self._get_system_prompt(profile_analysis, example_posts, learned_lessons, post_type, post_type_analysis, company_strategy, strategy_weight)
|
||||||
|
|
||||||
# Generate drafts in parallel with different temperatures/approaches
|
# Generate drafts in parallel with different temperatures/approaches
|
||||||
draft_configs = [
|
draft_configs = [
|
||||||
@@ -500,7 +507,8 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
|
|||||||
post_type_analysis: Optional[Dict[str, Any]] = None,
|
post_type_analysis: Optional[Dict[str, Any]] = None,
|
||||||
user_thoughts: str = "",
|
user_thoughts: str = "",
|
||||||
selected_hook: str = "",
|
selected_hook: str = "",
|
||||||
company_strategy: Optional[Dict[str, Any]] = None
|
company_strategy: Optional[Dict[str, Any]] = None,
|
||||||
|
strategy_weight: float = 0.5
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Write a single draft (original behavior)."""
|
"""Write a single draft (original behavior)."""
|
||||||
# Select examples if not already selected
|
# Select examples if not already selected
|
||||||
@@ -515,7 +523,7 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
|
|||||||
elif len(selected_examples) > 3:
|
elif len(selected_examples) > 3:
|
||||||
selected_examples = random.sample(selected_examples, 3)
|
selected_examples = random.sample(selected_examples, 3)
|
||||||
|
|
||||||
system_prompt = self._get_system_prompt(profile_analysis, selected_examples, learned_lessons, post_type, post_type_analysis, company_strategy)
|
system_prompt = self._get_system_prompt(profile_analysis, selected_examples, learned_lessons, post_type, post_type_analysis, company_strategy, strategy_weight)
|
||||||
user_prompt = self._get_user_prompt(topic, feedback, previous_version, critic_result, user_thoughts, selected_hook)
|
user_prompt = self._get_user_prompt(topic, feedback, previous_version, critic_result, user_thoughts, selected_hook)
|
||||||
|
|
||||||
# OPTIMIERT: Niedrigere Temperature (0.5 statt 0.6) für konsistenteren Stil
|
# OPTIMIERT: Niedrigere Temperature (0.5 statt 0.6) für konsistenteren Stil
|
||||||
@@ -536,7 +544,8 @@ Analysiere jeden Entwurf kurz und wähle den besten. Antworte im JSON-Format:
|
|||||||
learned_lessons: Optional[Dict[str, Any]] = None,
|
learned_lessons: Optional[Dict[str, Any]] = None,
|
||||||
post_type: Any = None,
|
post_type: Any = None,
|
||||||
post_type_analysis: Optional[Dict[str, Any]] = None,
|
post_type_analysis: Optional[Dict[str, Any]] = None,
|
||||||
company_strategy: Optional[Dict[str, Any]] = None
|
company_strategy: Optional[Dict[str, Any]] = None,
|
||||||
|
strategy_weight: float = 0.5
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get system prompt for writer - orientiert an bewährten n8n-Prompts."""
|
"""Get system prompt for writer - orientiert an bewährten n8n-Prompts."""
|
||||||
# Extract key profile information
|
# Extract key profile information
|
||||||
@@ -788,13 +797,13 @@ Vermeide IMMER diese KI-typischen Muster:
|
|||||||
- Zu perfekte, glatte Formulierungen - echte Menschen schreiben mit Ecken und Kanten
|
- Zu perfekte, glatte Formulierungen - echte Menschen schreiben mit Ecken und Kanten
|
||||||
{lessons_section}
|
{lessons_section}
|
||||||
{post_type_section}
|
{post_type_section}
|
||||||
{self._get_company_strategy_section(company_strategy)}
|
{self._get_company_strategy_section(company_strategy, strategy_weight)}
|
||||||
DEIN AUFTRAG: Schreibe den Post so, dass er für die Zielgruppe ({audience.get('target_audience', 'Professionals')}) einen klaren Mehrwert bietet und ihre Pain Points ({pain_points_str}) adressiert. Mach die Persönlichkeit des linguistischen Fingerabdrucks spürbar.
|
DEIN AUFTRAG: Schreibe den Post so, dass er für die Zielgruppe ({audience.get('target_audience', 'Professionals')}) einen klaren Mehrwert bietet und ihre Pain Points ({pain_points_str}) adressiert. Mach die Persönlichkeit des linguistischen Fingerabdrucks spürbar.
|
||||||
|
|
||||||
Beginne DIREKT mit dem Hook. Keine einleitenden Sätze, kein "Hier ist der Post"."""
|
Beginne DIREKT mit dem Hook. Keine einleitenden Sätze, kein "Hier ist der Post"."""
|
||||||
|
|
||||||
def _get_company_strategy_section(self, company_strategy: Optional[Dict[str, Any]] = None) -> str:
|
def _get_company_strategy_section(self, company_strategy: Optional[Dict[str, Any]] = None, strategy_weight: float = 0.5) -> str:
|
||||||
"""Build the company strategy section for the system prompt."""
|
"""Build the company strategy section for the system prompt with weighted influence."""
|
||||||
if not company_strategy:
|
if not company_strategy:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -812,11 +821,29 @@ Beginne DIREKT mit dem Hook. Keine einleitenden Sätze, kein "Hier ist der Post"
|
|||||||
if not any([mission, vision, brand_voice, content_pillars, dos, donts]):
|
if not any([mission, vision, brand_voice, content_pillars, dos, donts]):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
section = """
|
# Determine importance level and instruction based on strategy_weight
|
||||||
|
if strategy_weight >= 0.8:
|
||||||
|
importance = "KRITISCH WICHTIG - STRIKT BEFOLGEN"
|
||||||
|
instruction = "Der Post MUSS sich eng an die Unternehmensstrategie halten! Die Strategy-Guidelines haben höchste Priorität."
|
||||||
|
elif strategy_weight >= 0.6:
|
||||||
|
importance = "WICHTIG - BEACHTEN"
|
||||||
|
instruction = "Integriere die Unternehmenswerte deutlich, aber bewahre Authentizität! Die Strategie soll klar erkennbar sein."
|
||||||
|
elif strategy_weight >= 0.4:
|
||||||
|
importance = "ALS KONTEXT BERÜCKSICHTIGEN"
|
||||||
|
instruction = "Berücksichtige die Strategie wo sinnvoll - Persönlicher Stil hat Priorität! Subtile Integration genügt."
|
||||||
|
elif strategy_weight >= 0.2:
|
||||||
|
importance = "OPTIONALE ORIENTIERUNG"
|
||||||
|
instruction = "Die Strategie dient als Hintergrundinformation - freie Interpretation erlaubt! Fokus auf Authentizität."
|
||||||
|
else:
|
||||||
|
importance = "NUR ALS REFERENZ"
|
||||||
|
instruction = "Ignoriere die Strategie weitgehend - voller Fokus auf persönlichen Stil! Strategy-Guidelines sind optional."
|
||||||
|
|
||||||
8. UNTERNEHMENSSTRATEGIE (WICHTIG - IMMER BEACHTEN!):
|
section = f"""
|
||||||
|
|
||||||
Der Post muss zur Unternehmensstrategie passen, aber authentisch im Stil des Autors bleiben!
|
8. UNTERNEHMENSSTRATEGIE ({importance}):
|
||||||
|
|
||||||
|
Strategy Weight: {strategy_weight:.1f} / 1.0
|
||||||
|
{instruction}
|
||||||
"""
|
"""
|
||||||
if mission:
|
if mission:
|
||||||
section += f"\nMISSION: {mission}"
|
section += f"\nMISSION: {mission}"
|
||||||
@@ -835,7 +862,15 @@ Der Post muss zur Unternehmensstrategie passen, aber authentisch im Stil des Aut
|
|||||||
if donts:
|
if donts:
|
||||||
section += f"\n\nDON'Ts (vermeiden):\n" + "\n".join([f" - {d}" for d in donts])
|
section += f"\n\nDON'Ts (vermeiden):\n" + "\n".join([f" - {d}" for d in donts])
|
||||||
|
|
||||||
section += "\n\nWICHTIG: Integriere die Unternehmenswerte subtil - der Post soll nicht wie eine Werbung klingen!"
|
# Weight-specific closing instruction
|
||||||
|
if strategy_weight >= 0.8:
|
||||||
|
section += "\n\nKRITISCH: Die Unternehmensstrategie ist nicht verhandelbar - jeder Post muss diese widerspiegeln!"
|
||||||
|
elif strategy_weight >= 0.6:
|
||||||
|
section += "\n\nWICHTIG: Stelle sicher, dass die Unternehmenswerte deutlich erkennbar sind!"
|
||||||
|
elif strategy_weight >= 0.4:
|
||||||
|
section += "\n\nINTEGRIERE: Berücksichtige die Strategie subtil - authentischer Stil bleibt wichtig!"
|
||||||
|
else:
|
||||||
|
section += "\n\nREFERENZ: Die Strategie dient nur als Orientierung - persönlicher Stil hat Vorrang!"
|
||||||
|
|
||||||
return section
|
return section
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ class PostType(DBModel):
|
|||||||
analysis_generated_at: Optional[datetime] = None
|
analysis_generated_at: Optional[datetime] = None
|
||||||
analyzed_post_count: int = 0
|
analyzed_post_count: int = 0
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
|
strategy_weight: float = 0.5
|
||||||
|
|
||||||
|
|
||||||
class LinkedInProfile(DBModel):
|
class LinkedInProfile(DBModel):
|
||||||
|
|||||||
@@ -342,11 +342,13 @@ class WorkflowOrchestrator:
|
|||||||
# Get post type context if specified
|
# Get post type context if specified
|
||||||
post_type = None
|
post_type = None
|
||||||
post_type_analysis = None
|
post_type_analysis = None
|
||||||
|
strategy_weight = 0.5 # Default strategy weight
|
||||||
if post_type_id:
|
if post_type_id:
|
||||||
post_type = await db.get_post_type(post_type_id)
|
post_type = await db.get_post_type(post_type_id)
|
||||||
if post_type:
|
if post_type:
|
||||||
post_type_analysis = post_type.analysis
|
post_type_analysis = post_type.analysis
|
||||||
logger.info(f"Targeting research for post type: {post_type.name}")
|
strategy_weight = post_type.strategy_weight
|
||||||
|
logger.info(f"Targeting research for post type: {post_type.name} with strategy weight {strategy_weight:.1f}")
|
||||||
|
|
||||||
def report_progress(message: str, step: int, total: int = 4):
|
def report_progress(message: str, step: int, total: int = 4):
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
@@ -358,6 +360,15 @@ class WorkflowOrchestrator:
|
|||||||
if not profile_analysis:
|
if not profile_analysis:
|
||||||
raise ValueError("Profile analysis not found. Run initial setup first.")
|
raise ValueError("Profile analysis not found. Run initial setup first.")
|
||||||
|
|
||||||
|
# Step 1.5: Load company strategy if user belongs to a company
|
||||||
|
company_strategy = None
|
||||||
|
profile = await db.get_profile(user_id)
|
||||||
|
if profile and profile.company_id:
|
||||||
|
company = await db.get_company(profile.company_id)
|
||||||
|
if company and company.company_strategy:
|
||||||
|
company_strategy = company.company_strategy
|
||||||
|
logger.info(f"Loaded company strategy for research: {company.name}")
|
||||||
|
|
||||||
# Step 2: Get ALL existing topics (from multiple sources to avoid repetition)
|
# Step 2: Get ALL existing topics (from multiple sources to avoid repetition)
|
||||||
report_progress("Lade existierende Topics...", 2)
|
report_progress("Lade existierende Topics...", 2)
|
||||||
existing_topics = set()
|
existing_topics = set()
|
||||||
@@ -384,9 +395,6 @@ class WorkflowOrchestrator:
|
|||||||
existing_topics = list(existing_topics)
|
existing_topics = list(existing_topics)
|
||||||
logger.info(f"Found {len(existing_topics)} existing topics to avoid")
|
logger.info(f"Found {len(existing_topics)} existing topics to avoid")
|
||||||
|
|
||||||
# Get profile data
|
|
||||||
profile = await db.get_profile(user_id)
|
|
||||||
|
|
||||||
# Get example posts to understand the person's actual content style
|
# Get example posts to understand the person's actual content style
|
||||||
# If post_type_id is specified, only use posts of that type
|
# If post_type_id is specified, only use posts of that type
|
||||||
if post_type_id:
|
if post_type_id:
|
||||||
@@ -409,7 +417,9 @@ class WorkflowOrchestrator:
|
|||||||
customer_data=profile.metadata,
|
customer_data=profile.metadata,
|
||||||
example_posts=example_post_texts,
|
example_posts=example_post_texts,
|
||||||
post_type=post_type,
|
post_type=post_type,
|
||||||
post_type_analysis=post_type_analysis
|
post_type_analysis=post_type_analysis,
|
||||||
|
company_strategy=company_strategy,
|
||||||
|
strategy_weight=strategy_weight
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 4: Save research results
|
# Step 4: Save research results
|
||||||
@@ -582,11 +592,14 @@ class WorkflowOrchestrator:
|
|||||||
# Get post type info if specified
|
# Get post type info if specified
|
||||||
post_type = None
|
post_type = None
|
||||||
post_type_analysis = None
|
post_type_analysis = None
|
||||||
|
strategy_weight = 0.5 # Default strategy weight
|
||||||
if post_type_id:
|
if post_type_id:
|
||||||
post_type = await db.get_post_type(post_type_id)
|
post_type = await db.get_post_type(post_type_id)
|
||||||
if post_type and post_type.analysis:
|
if post_type:
|
||||||
post_type_analysis = post_type.analysis
|
if post_type.analysis:
|
||||||
logger.info(f"Using post type '{post_type.name}' for writing")
|
post_type_analysis = post_type.analysis
|
||||||
|
strategy_weight = post_type.strategy_weight # Extract strategy weight from post type
|
||||||
|
logger.info(f"Using post type '{post_type.name}' with strategy weight {strategy_weight:.1f}")
|
||||||
|
|
||||||
# Load user's real posts as style examples
|
# Load user's real posts as style examples
|
||||||
# If post_type_id is specified, only use posts of that type
|
# If post_type_id is specified, only use posts of that type
|
||||||
@@ -642,7 +655,8 @@ class WorkflowOrchestrator:
|
|||||||
post_type_analysis=post_type_analysis,
|
post_type_analysis=post_type_analysis,
|
||||||
user_thoughts=user_thoughts,
|
user_thoughts=user_thoughts,
|
||||||
selected_hook=selected_hook,
|
selected_hook=selected_hook,
|
||||||
company_strategy=company_strategy # NEW: Pass company strategy
|
company_strategy=company_strategy, # Pass company strategy
|
||||||
|
strategy_weight=strategy_weight # NEW: Pass strategy weight
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Revision based on feedback - pass full critic result for structured changes
|
# Revision based on feedback - pass full critic result for structured changes
|
||||||
@@ -660,7 +674,8 @@ class WorkflowOrchestrator:
|
|||||||
post_type_analysis=post_type_analysis,
|
post_type_analysis=post_type_analysis,
|
||||||
user_thoughts=user_thoughts,
|
user_thoughts=user_thoughts,
|
||||||
selected_hook=selected_hook,
|
selected_hook=selected_hook,
|
||||||
company_strategy=company_strategy # NEW: Pass company strategy
|
company_strategy=company_strategy, # Pass company strategy
|
||||||
|
strategy_weight=strategy_weight # NEW: Pass strategy weight
|
||||||
)
|
)
|
||||||
|
|
||||||
writer_versions.append(current_post)
|
writer_versions.append(current_post)
|
||||||
|
|||||||
@@ -396,6 +396,101 @@ async def run_post_categorization(user_id: UUID, job_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_post_recategorization(user_id: UUID, job_id: str):
|
||||||
|
"""Re-categorize ALL LinkedIn posts (including already categorized ones)."""
|
||||||
|
from src.database.client import DatabaseClient
|
||||||
|
from src.agents.post_classifier import PostClassifierAgent
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
db = DatabaseClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
status=JobStatus.RUNNING,
|
||||||
|
progress=5,
|
||||||
|
message="Lade alle LinkedIn Posts..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get ALL LinkedIn posts (not just unclassified)
|
||||||
|
posts = await db.get_linkedin_posts(user_id)
|
||||||
|
post_types = await db.get_post_types(user_id)
|
||||||
|
|
||||||
|
if not posts:
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
status=JobStatus.COMPLETED,
|
||||||
|
progress=100,
|
||||||
|
message="Keine Posts gefunden!"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not post_types:
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
status=JobStatus.FAILED,
|
||||||
|
error="Keine Post-Typen definiert"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# FIRST: Mark all posts as uncategorized before re-categorizing
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
progress=15,
|
||||||
|
message=f"Markiere {len(posts)} Posts als unkategorisiert..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set post_type_id to NULL for all posts
|
||||||
|
for post in posts:
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(
|
||||||
|
lambda p=post: db.client.table("linkedin_posts").update({
|
||||||
|
"post_type_id": None
|
||||||
|
}).eq("id", str(p.id)).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to unclassify post {post.id}: {e}")
|
||||||
|
|
||||||
|
logger.info(f"Marked {len(posts)} posts as uncategorized")
|
||||||
|
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
progress=30,
|
||||||
|
message=f"Re-kategorisiere {len(posts)} Posts..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run classification on ALL posts
|
||||||
|
classifier = PostClassifierAgent()
|
||||||
|
classifications = await classifier.process(posts, post_types)
|
||||||
|
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
progress=70,
|
||||||
|
message="Speichere neue Kategorisierungen..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save classifications
|
||||||
|
if classifications:
|
||||||
|
await db.update_posts_classification_bulk(classifications)
|
||||||
|
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
status=JobStatus.COMPLETED,
|
||||||
|
progress=100,
|
||||||
|
message=f"{len(classifications)} Posts re-kategorisiert!"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Post re-categorization completed for user {user_id}: {len(classifications)} posts")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Post re-categorization failed: {e}")
|
||||||
|
await job_manager.update_job(
|
||||||
|
job_id,
|
||||||
|
status=JobStatus.FAILED,
|
||||||
|
error=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def run_post_type_analysis(user_id: UUID, job_id: str):
|
async def run_post_type_analysis(user_id: UUID, job_id: str):
|
||||||
"""Run post type analysis in background."""
|
"""Run post type analysis in background."""
|
||||||
from src.database.client import DatabaseClient
|
from src.database.client import DatabaseClient
|
||||||
|
|||||||
@@ -77,11 +77,11 @@
|
|||||||
{% for employee in employees %}
|
{% for employee in employees %}
|
||||||
<div class="flex items-center justify-between p-4 bg-brand-bg border border-gray-600 rounded-lg">
|
<div class="flex items-center justify-between p-4 bg-brand-bg border border-gray-600 rounded-lg">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-10 h-10 rounded-full overflow-hidden bg-brand-highlight flex items-center justify-center">
|
<div class="w-10 h-10 rounded-full bg-brand-highlight/20 flex items-center justify-center overflow-hidden">
|
||||||
{% if employee.linkedin_picture %}
|
{% if employee.linkedin_picture %}
|
||||||
<img src="{{ employee.linkedin_picture }}" alt="{{ employee.linkedin_name }}" class="w-full h-full object-cover" referrerpolicy="no-referrer">
|
<img src="{{ employee.linkedin_picture }}" alt="" class="w-10 h-10 rounded-full">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-brand-bg-dark font-bold">{{ (employee.display_name or employee.linkedin_name or employee.email)[0] | upper }}</span>
|
<span class="text-brand-highlight font-bold">{{ (employee.display_name or employee.linkedin_name or employee.email)[0] | upper }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="card-bg border rounded-xl p-6 mb-8">
|
<div class="card-bg border rounded-xl p-6 mb-8">
|
||||||
<h2 class="text-lg font-semibold text-white mb-4">Schnellzugriff</h2>
|
<h2 class="text-lg font-semibold text-white mb-4">Schnellzugriff</h2>
|
||||||
<div class="grid md:grid-cols-3 gap-4">
|
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<a href="/research" class="flex items-center gap-3 p-4 bg-brand-bg-dark rounded-lg hover:bg-brand-bg-light transition-colors">
|
<a href="/research" class="flex items-center gap-3 p-4 bg-brand-bg-dark 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">
|
<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">
|
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
955
src/web/templates/user/employee_post_types.html
Normal file
955
src/web/templates/user/employee_post_types.html
Normal file
@@ -0,0 +1,955 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Post-Typen verwalten{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<a href="#" onclick="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 verwalten</h1>
|
||||||
|
<p class="text-gray-400 mt-1">Definiere und konfiguriere deine 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>
|
||||||
|
|
||||||
|
<!-- Strategy Info Banner -->
|
||||||
|
{% if has_strategy %}
|
||||||
|
<div class="bg-brand-highlight/10 border border-brand-highlight/30 rounded-xl p-4 mb-6">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<svg class="w-5 h-5 text-brand-highlight flex-shrink-0 mt-0.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>
|
||||||
|
<div>
|
||||||
|
<p class="text-brand-highlight font-medium mb-1">Strategy Weight erklärt</p>
|
||||||
|
<p class="text-gray-300 text-sm">
|
||||||
|
Der <strong>Strategy Weight</strong> steuert, wie stark die KI bei der Post-Generierung die Unternehmensstrategie berücksichtigt:
|
||||||
|
<span class="text-gray-400">0.0 = komplett ignorieren</span> |
|
||||||
|
<span class="text-gray-300">0.5 = ausgewogen</span> |
|
||||||
|
<span class="text-brand-highlight">1.0 = strikt befolgen</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- 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 deinen ersten Post-Typ, um mit der Kategorisierung zu beginnen.</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">
|
||||||
|
<!-- Will be rendered by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Initial data from server (hidden div as backup) -->
|
||||||
|
<div id="initial-post-types" style="display:none;" data-json="{{ post_types_json | escape }}"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Load initial data
|
||||||
|
const INITIAL_POST_TYPES = {{ post_types_json | safe }};
|
||||||
|
console.log('Loaded INITIAL_POST_TYPES:', INITIAL_POST_TYPES);
|
||||||
|
|
||||||
|
// ========== 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate names (case-insensitive, only active post types)
|
||||||
|
const activePostTypes = postTypesState.filter(pt => pt._status !== 'deleted');
|
||||||
|
const duplicateName = activePostTypes.find(pt => {
|
||||||
|
// When editing, allow same name for the current post type
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('delete-modal').classList.add('hidden');
|
||||||
|
document.getElementById('delete-modal').classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== TOAST SYSTEM ==========
|
||||||
|
|
||||||
|
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 %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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 erstellen
|
||||||
|
</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">
|
||||||
|
<!-- Icon will be inserted by JS -->
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
<parameter>
|
||||||
|
</invoke>
|
||||||
|
/* Custom slider styles */
|
||||||
|
.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::-moz-range-thumb:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(250, 204, 21, 0.6);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 - all changes happen in memory until Save
|
||||||
|
let postTypesState = [];
|
||||||
|
let originalPostTypesJSON = '';
|
||||||
|
let hasUnsavedChanges = false;
|
||||||
|
let isEditMode = false;
|
||||||
|
let currentEditId = null;
|
||||||
|
let tempIdCounter = 0;
|
||||||
|
let isIntentionalNavigation = false; // Flag to prevent double warning
|
||||||
|
|
||||||
|
// Initialize state from server data
|
||||||
|
function initializeState() {
|
||||||
|
console.log('Initializing state...');
|
||||||
|
|
||||||
|
// Use the direct JavaScript variable
|
||||||
|
if (typeof INITIAL_POST_TYPES === 'undefined') {
|
||||||
|
console.error('INITIAL_POST_TYPES not defined!');
|
||||||
|
showToast('Fehler: Daten konnten nicht geladen werden', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = INITIAL_POST_TYPES;
|
||||||
|
console.log('Loaded data:', data);
|
||||||
|
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('State created:', postTypesState);
|
||||||
|
originalPostTypesJSON = JSON.stringify(postTypesState);
|
||||||
|
renderPostTypesList();
|
||||||
|
updateSaveButton();
|
||||||
|
|
||||||
|
// No toast on successful load - too noisy
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize state:', e);
|
||||||
|
console.error('Stack:', e.stack);
|
||||||
|
showToast('Fehler beim Laden der Post-Typen: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are unsaved changes
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the post types list from state
|
||||||
|
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 deinen ersten Post-Typ, um mit der Kategorisierung zu beginnen.</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="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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update save button state
|
||||||
|
function updateSaveButton() {
|
||||||
|
const saveBtn = document.getElementById('save-all-btn');
|
||||||
|
if (!saveBtn) return;
|
||||||
|
|
||||||
|
const activeTypes = postTypesState.filter(pt => pt._status !== 'deleted');
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new post type (only in state)
|
||||||
|
function createPostTypeState(name, description, strategyWeight) {
|
||||||
|
const tempId = `temp_${++tempIdCounter}`;
|
||||||
|
postTypesState.push({
|
||||||
|
id: tempId,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
strategy_weight: strategyWeight,
|
||||||
|
post_count: 0,
|
||||||
|
_status: 'new',
|
||||||
|
_originalWeight: strategyWeight
|
||||||
|
});
|
||||||
|
|
||||||
|
renderPostTypesList();
|
||||||
|
checkForChanges();
|
||||||
|
// No toast - change is visible in UI with badge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit post type (only in state)
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update post type in state
|
||||||
|
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();
|
||||||
|
// No toast - change is visible in UI with badge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete post type (only in state)
|
||||||
|
function deletePostTypeState(id) {
|
||||||
|
const activeTypes = postTypesState.filter(pt => pt._status !== 'deleted');
|
||||||
|
|
||||||
|
// Prevent deleting last post type
|
||||||
|
if (activeTypes.length === 1) {
|
||||||
|
showToast('Du musst mindestens einen Post-Typ behalten!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === id);
|
||||||
|
if (!postType) return;
|
||||||
|
|
||||||
|
// Show confirmation if post type has assigned posts
|
||||||
|
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. Möchtest du wirklich löschen?`,
|
||||||
|
confirmText: 'Löschen',
|
||||||
|
onConfirm: () => {
|
||||||
|
performDelete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
performDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function performDelete(id) {
|
||||||
|
const postType = postTypesState.find(pt => pt.id === id);
|
||||||
|
if (!postType) return;
|
||||||
|
|
||||||
|
if (postType._status === 'new') {
|
||||||
|
// Remove completely if it was never saved
|
||||||
|
postTypesState = postTypesState.filter(pt => pt.id !== id);
|
||||||
|
} else {
|
||||||
|
// Mark as deleted
|
||||||
|
postType._status = 'deleted';
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPostTypesList();
|
||||||
|
checkForChanges();
|
||||||
|
// No toast - change is visible in UI (item removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update strategy weight in state
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all changes to database
|
||||||
|
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('Du musst mindestens einen Post-Typ behalten!', '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/employee/post-types/${pt.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/employee/post-types', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
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/employee/post-types/${pt.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: pt.name,
|
||||||
|
description: pt.description,
|
||||||
|
strategy_weight: pt.strategy_weight
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Trigger save-all with structural changes flag
|
||||||
|
const saveAllResponse = await fetch('/api/employee/post-types/save-all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ has_structural_changes: hasStructuralChanges })
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveAllResult = await saveAllResponse.json();
|
||||||
|
|
||||||
|
if (saveAllResult.success) {
|
||||||
|
// Update state to mark everything as saved
|
||||||
|
postTypesState = postTypesState.filter(pt => pt._status !== 'deleted');
|
||||||
|
postTypesState.forEach(pt => {
|
||||||
|
pt._status = 'existing';
|
||||||
|
pt._originalWeight = pt.strategy_weight;
|
||||||
|
});
|
||||||
|
|
||||||
|
originalPostTypesJSON = JSON.stringify(postTypesState);
|
||||||
|
hasUnsavedChanges = false;
|
||||||
|
|
||||||
|
// Re-render to remove status badges
|
||||||
|
renderPostTypesList();
|
||||||
|
updateSaveButton();
|
||||||
|
|
||||||
|
if (saveAllResult.recategorized) {
|
||||||
|
showToast('Gespeichert - Rekategorisierung läuft im Hintergrund', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('Änderungen gespeichert', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// No reload needed - UI is already up to date!
|
||||||
|
} else {
|
||||||
|
throw new Error(saveAllResult.error || 'Save failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving changes:', error);
|
||||||
|
showToast('Fehler beim Speichern: ' + error.message, 'error');
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== CONFIRMATION MODAL ==========
|
||||||
|
let confirmationCallback = null;
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Set content
|
||||||
|
title.textContent = options.title || 'Bestätigung';
|
||||||
|
message.textContent = options.message || 'Bist du sicher?';
|
||||||
|
|
||||||
|
// Set icon
|
||||||
|
const iconType = options.type || 'warning'; // warning, danger, info
|
||||||
|
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 if (iconType === 'warning') {
|
||||||
|
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';
|
||||||
|
} else {
|
||||||
|
icon.className = 'w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 bg-blue-500/20';
|
||||||
|
icon.innerHTML = `<svg class="w-6 h-6 text-blue-400" 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>`;
|
||||||
|
confirmBtn.className = 'flex-1 px-4 py-2.5 font-medium rounded-lg transition-colors bg-brand-highlight hover:bg-yellow-500 text-black';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set button text
|
||||||
|
confirmBtn.textContent = options.confirmText || 'Bestätigen';
|
||||||
|
|
||||||
|
// Store callback
|
||||||
|
confirmationCallback = options.onConfirm || null;
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeConfirmationModal() {
|
||||||
|
const modal = document.getElementById('confirmation-modal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
confirmationCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeConfirmation() {
|
||||||
|
if (confirmationCallback) {
|
||||||
|
confirmationCallback();
|
||||||
|
}
|
||||||
|
closeConfirmationModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle back navigation with unsaved changes warning
|
||||||
|
function handleBackNavigation(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
showConfirmation({
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Ungespeicherte Änderungen',
|
||||||
|
message: 'Du hast ungespeicherte Änderungen. Wenn du jetzt zurückgehst, gehen diese verloren. Möchtest du wirklich fortfahren?',
|
||||||
|
confirmText: 'Trotzdem zurück',
|
||||||
|
onConfirm: () => {
|
||||||
|
// Set flag to prevent beforeunload from showing browser dialog
|
||||||
|
isIntentionalNavigation = true;
|
||||||
|
window.location.href = '/post-types';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No unsaved changes, navigate directly
|
||||||
|
isIntentionalNavigation = true;
|
||||||
|
window.location.href = '/post-types';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initializeState();
|
||||||
|
|
||||||
|
// Warn before leaving with unsaved changes (only for browser events like refresh/close)
|
||||||
|
window.addEventListener('beforeunload', (e) => {
|
||||||
|
// Don't show browser warning if this is intentional navigation (already confirmed in custom modal)
|
||||||
|
if (hasUnsavedChanges && !isIntentionalNavigation) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
706
src/web/templates/user/employee_post_types.html.backup
Normal file
706
src/web/templates/user/employee_post_types.html.backup
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Post-Typen verwalten{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-white">Post-Typen verwalten</h1>
|
||||||
|
<p class="text-gray-400 mt-1">Definiere und konfiguriere deine 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>
|
||||||
|
|
||||||
|
<!-- Strategy Info Banner -->
|
||||||
|
{% if has_strategy %}
|
||||||
|
<div class="bg-brand-highlight/10 border border-brand-highlight/30 rounded-xl p-4 mb-6">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<svg class="w-5 h-5 text-brand-highlight flex-shrink-0 mt-0.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>
|
||||||
|
<div>
|
||||||
|
<p class="text-brand-highlight font-medium mb-1">Strategy Weight erklärt</p>
|
||||||
|
<p class="text-gray-300 text-sm">
|
||||||
|
Der <strong>Strategy Weight</strong> steuert, wie stark die KI bei der Post-Generierung die Unternehmensstrategie berücksichtigt:
|
||||||
|
<span class="text-gray-400">0.0 = komplett ignorieren</span> |
|
||||||
|
<span class="text-gray-300">0.5 = ausgewogen</span> |
|
||||||
|
<span class="text-brand-highlight">1.0 = strikt befolgen</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- 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 deinen ersten Post-Typ, um mit der Kategorisierung zu beginnen.</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">
|
||||||
|
{% for item in post_types_with_counts %}
|
||||||
|
{% set pt = item.post_type %}
|
||||||
|
<div class="card-bg border rounded-xl p-6" 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">{{ pt.name }}</h3>
|
||||||
|
{% if pt.description %}
|
||||||
|
<p class="text-gray-400 text-sm">{{ pt.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button onclick="editPostType('{{ pt.id }}', '{{ pt.name }}', '{{ pt.description or '' }}', {{ pt.strategy_weight }})"
|
||||||
|
class="px-3 py-2 text-sm text-gray-300 hover:text-white hover:bg-brand-bg-light rounded-lg transition-colors">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button onclick="deletePostType('{{ 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>
|
||||||
|
|
||||||
|
<!-- Strategy Weight Slider -->
|
||||||
|
<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 }}">{{ "%.1f"|format(pt.strategy_weight) }}</span>
|
||||||
|
</label>
|
||||||
|
<input type="range"
|
||||||
|
id="weight-{{ pt.id }}"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
value="{{ pt.strategy_weight }}"
|
||||||
|
onchange="updateStrategyWeight('{{ 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>
|
||||||
|
|
||||||
|
<!-- Post Count -->
|
||||||
|
<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>{{ item.post_count }}</strong> Posts zugeordnet</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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()"
|
||||||
|
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 %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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('Thought 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">Thought 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. Thought 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 erstellen
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Custom slider styles */
|
||||||
|
.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::-moz-range-thumb:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(250, 204, 21, 0.6);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 originalPostTypes = []; // Original state from backend
|
||||||
|
let currentPostTypes = []; // Current working state
|
||||||
|
let tempIdCounter = 0; // For temporary IDs of new post types
|
||||||
|
let hasUnsavedChanges = false;
|
||||||
|
let hasStructuralChanges = false;
|
||||||
|
let isEditMode = false;
|
||||||
|
let currentEditId = null;
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadInitialState();
|
||||||
|
setupBeforeUnloadWarning();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load initial state from DOM
|
||||||
|
function loadInitialState() {
|
||||||
|
const postTypeCards = document.querySelectorAll('[data-post-type-id]');
|
||||||
|
originalPostTypes = [];
|
||||||
|
|
||||||
|
postTypeCards.forEach(card => {
|
||||||
|
const id = card.dataset.postTypeId;
|
||||||
|
const name = card.querySelector('h3').textContent.trim();
|
||||||
|
const description = card.querySelector('p.text-gray-400')?.textContent.trim() || '';
|
||||||
|
const weightValue = card.querySelector('[id^="weight-value-"]').textContent.trim();
|
||||||
|
const weight = parseFloat(weightValue);
|
||||||
|
const postCount = parseInt(card.querySelector('strong').textContent);
|
||||||
|
|
||||||
|
originalPostTypes.push({
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
strategy_weight: weight,
|
||||||
|
post_count: postCount,
|
||||||
|
is_new: false,
|
||||||
|
is_deleted: false,
|
||||||
|
is_modified: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clone for working state
|
||||||
|
currentPostTypes = JSON.parse(JSON.stringify(originalPostTypes));
|
||||||
|
updateSaveButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup warning when leaving page with unsaved changes
|
||||||
|
function setupBeforeUnloadWarning() {
|
||||||
|
window.addEventListener('beforeunload', (e) => {
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open create modal
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Show step 1, hide step 2
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select predefined type
|
||||||
|
function selectPredefinedType(typeName) {
|
||||||
|
const descriptions = {
|
||||||
|
'Thought 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] || '';
|
||||||
|
|
||||||
|
// Go to step 2
|
||||||
|
document.getElementById('modal-step-1').classList.add('hidden');
|
||||||
|
document.getElementById('edit-form').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to custom type
|
||||||
|
function goToCustomType() {
|
||||||
|
document.getElementById('edit-name').value = '';
|
||||||
|
document.getElementById('edit-description').value = '';
|
||||||
|
|
||||||
|
// Go to step 2
|
||||||
|
document.getElementById('modal-step-1').classList.add('hidden');
|
||||||
|
document.getElementById('edit-form').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back to step 1
|
||||||
|
function backToStep1() {
|
||||||
|
document.getElementById('modal-step-1').classList.remove('hidden');
|
||||||
|
document.getElementById('edit-form').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit post type
|
||||||
|
function editPostType(id, name, description, weight) {
|
||||||
|
document.getElementById('modal-title').textContent = 'Post-Typ bearbeiten';
|
||||||
|
document.getElementById('edit-id').value = id;
|
||||||
|
document.getElementById('edit-name').value = name;
|
||||||
|
document.getElementById('edit-description').value = description;
|
||||||
|
document.getElementById('edit-weight').value = weight;
|
||||||
|
updateModalWeightDisplay(weight);
|
||||||
|
isEditMode = true;
|
||||||
|
currentEditId = id;
|
||||||
|
|
||||||
|
// Skip step 1 when editing, go straight to form
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close edit modal
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('edit-modal').classList.add('hidden');
|
||||||
|
document.getElementById('edit-modal').classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update modal weight display
|
||||||
|
function updateModalWeightDisplay(value) {
|
||||||
|
document.getElementById('modal-weight-value').textContent = parseFloat(value).toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit post type (create or update)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
name: name,
|
||||||
|
description: description || null,
|
||||||
|
strategy_weight: strategyWeight
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
|
if (isEditMode && id) {
|
||||||
|
// Update existing
|
||||||
|
response = await fetch(`/api/employee/post-types/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
response = await fetch('/api/employee/post-types', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Mark that structural changes happened (only for create, not edit)
|
||||||
|
if (!isEditMode) {
|
||||||
|
hasStructuralChanges = true;
|
||||||
|
localStorage.setItem('postTypesStructuralChanges', 'true');
|
||||||
|
}
|
||||||
|
showToast(isEditMode ? 'Post-Typ aktualisiert' : 'Post-Typ erstellt', 'success');
|
||||||
|
closeEditModal();
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
} else {
|
||||||
|
showToast(result.error || 'Ein Fehler ist aufgetreten', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting post type:', error);
|
||||||
|
showToast('Verbindungsfehler beim Speichern', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update strategy weight
|
||||||
|
function updateStrategyWeight(id, value) {
|
||||||
|
const numValue = parseFloat(value);
|
||||||
|
document.getElementById(`weight-value-${id}`).textContent = numValue.toFixed(1);
|
||||||
|
changedWeights.set(id, numValue);
|
||||||
|
|
||||||
|
// Update button text and highlight
|
||||||
|
updateSaveButtonText();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete post type
|
||||||
|
async function deletePostType(id) {
|
||||||
|
try {
|
||||||
|
// First check how many posts are affected
|
||||||
|
const response = await fetch(`/api/employee/post-types/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Mark that structural changes happened
|
||||||
|
hasStructuralChanges = true;
|
||||||
|
localStorage.setItem('postTypesStructuralChanges', 'true');
|
||||||
|
|
||||||
|
if (result.affected_posts > 0) {
|
||||||
|
// Show confirmation modal
|
||||||
|
document.getElementById('delete-id').value = id;
|
||||||
|
document.getElementById('delete-message').textContent =
|
||||||
|
`Dieser Post-Typ hat ${result.affected_posts} zugeordnete Posts. Wirklich löschen?`;
|
||||||
|
document.getElementById('delete-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('delete-modal').classList.add('flex');
|
||||||
|
} else {
|
||||||
|
// Direct delete
|
||||||
|
showToast('Post-Typ gelöscht', 'success');
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast(result.error || 'Fehler beim Löschen', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting post type:', error);
|
||||||
|
showToast('Verbindungsfehler', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm delete from modal
|
||||||
|
async function confirmDelete() {
|
||||||
|
const id = document.getElementById('delete-id').value;
|
||||||
|
closeDeleteModal();
|
||||||
|
|
||||||
|
// Delete was already done, just reload
|
||||||
|
showToast('Post-Typ gelöscht', 'success');
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close delete modal
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('delete-modal').classList.add('hidden');
|
||||||
|
document.getElementById('delete-modal').classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update save button text based on what changed
|
||||||
|
function updateSaveButtonText() {
|
||||||
|
const saveBtn = document.getElementById('save-all-btn');
|
||||||
|
if (!saveBtn) return;
|
||||||
|
|
||||||
|
const btnText = saveBtn.querySelector('span');
|
||||||
|
if (!btnText) return;
|
||||||
|
|
||||||
|
if (hasStructuralChanges) {
|
||||||
|
btnText.textContent = 'Speichern & Re-Kategorisieren';
|
||||||
|
saveBtn.classList.add('ring-4', 'ring-brand-highlight/50');
|
||||||
|
} else if (changedWeights.size > 0) {
|
||||||
|
btnText.textContent = 'Änderungen speichern';
|
||||||
|
saveBtn.classList.add('ring-4', 'ring-brand-highlight/50');
|
||||||
|
} else {
|
||||||
|
btnText.textContent = 'Änderungen speichern';
|
||||||
|
saveBtn.classList.remove('ring-4', 'ring-brand-highlight/50');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all changes (smart save - only re-categorize if structural changes)
|
||||||
|
async function saveAllChanges() {
|
||||||
|
const saveBtn = document.getElementById('save-all-btn');
|
||||||
|
if (!saveBtn) return;
|
||||||
|
|
||||||
|
// Check if there are any changes to save
|
||||||
|
if (changedWeights.size === 0 && !hasStructuralChanges) {
|
||||||
|
showToast('Keine Änderungen zu speichern', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save all weight changes first
|
||||||
|
if (changedWeights.size > 0) {
|
||||||
|
const updatePromises = Array.from(changedWeights.entries()).map(([id, weight]) => {
|
||||||
|
return fetch(`/api/employee/post-types/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ strategy_weight: weight })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(updatePromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger save-all with structural changes flag
|
||||||
|
const response = await fetch('/api/employee/post-types/save-all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
has_structural_changes: hasStructuralChanges
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
changedWeights.clear();
|
||||||
|
hasStructuralChanges = false;
|
||||||
|
localStorage.removeItem('postTypesStructuralChanges');
|
||||||
|
saveBtn.classList.remove('ring-4', 'ring-brand-highlight/50');
|
||||||
|
|
||||||
|
if (result.recategorized) {
|
||||||
|
showToast('Änderungen gespeichert - Rekategorisierung läuft...', 'success');
|
||||||
|
console.log('Re-categorization started:', result.categorization_job_id);
|
||||||
|
console.log('Analysis started:', result.analysis_job_id);
|
||||||
|
} else {
|
||||||
|
showToast('Änderungen gespeichert', 'success');
|
||||||
|
console.log('Only weights updated, no re-categorization');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload to show updated values
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showToast(result.error || 'Unbekannter Fehler', 'error');
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving changes:', error);
|
||||||
|
showToast('Verbindungsfehler: ' + error.message, 'error');
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast notification system
|
||||||
|
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>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
355
src/web/templates/user/employee_post_types_new.js
Normal file
355
src/web/templates/user/employee_post_types_new.js
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
// State management - all changes happen in memory until Save
|
||||||
|
let postTypesState = [];
|
||||||
|
let originalPostTypesJSON = '';
|
||||||
|
let hasUnsavedChanges = false;
|
||||||
|
let isEditMode = false;
|
||||||
|
let currentEditId = null;
|
||||||
|
let tempIdCounter = 0;
|
||||||
|
|
||||||
|
// Initialize state from server data
|
||||||
|
function initializeState() {
|
||||||
|
const initialData = document.getElementById('initial-post-types');
|
||||||
|
if (!initialData) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(initialData.textContent);
|
||||||
|
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', // 'existing', 'new', 'modified', 'deleted'
|
||||||
|
_originalWeight: item.post_type.strategy_weight
|
||||||
|
}));
|
||||||
|
originalPostTypesJSON = JSON.stringify(postTypesState);
|
||||||
|
renderPostTypesList();
|
||||||
|
updateSaveButton();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize state:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are unsaved changes
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the post types list from state
|
||||||
|
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 deinen ersten Post-Typ, um mit der Kategorisierung zu beginnen.</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-gray-300 hover:text-white hover:bg-brand-bg-light 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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update save button state
|
||||||
|
function updateSaveButton() {
|
||||||
|
const saveBtn = document.getElementById('save-all-btn');
|
||||||
|
if (!saveBtn) return;
|
||||||
|
|
||||||
|
const activeTypes = postTypesState.filter(pt => pt._status !== 'deleted');
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new post type (only in state)
|
||||||
|
function createPostTypeState(name, description, strategyWeight) {
|
||||||
|
const tempId = `temp_${++tempIdCounter}`;
|
||||||
|
postTypesState.push({
|
||||||
|
id: tempId,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
strategy_weight: strategyWeight,
|
||||||
|
post_count: 0,
|
||||||
|
_status: 'new',
|
||||||
|
_originalWeight: strategyWeight
|
||||||
|
});
|
||||||
|
|
||||||
|
renderPostTypesList();
|
||||||
|
checkForChanges();
|
||||||
|
showToast('Post-Typ hinzugefügt (noch nicht gespeichert)', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit post type (only in state)
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update post type in state
|
||||||
|
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();
|
||||||
|
showToast('Post-Typ aktualisiert (noch nicht gespeichert)', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete post type (only in state)
|
||||||
|
function deletePostTypeState(id) {
|
||||||
|
const activeTypes = postTypesState.filter(pt => pt._status !== 'deleted');
|
||||||
|
|
||||||
|
// Prevent deleting last post type
|
||||||
|
if (activeTypes.length === 1) {
|
||||||
|
showToast('Du musst mindestens einen Post-Typ behalten!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === id);
|
||||||
|
if (!postType) return;
|
||||||
|
|
||||||
|
if (postType.post_count > 0) {
|
||||||
|
if (!confirm(`Dieser Post-Typ hat ${postType.post_count} zugeordnete Posts. Wirklich löschen?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postType._status === 'new') {
|
||||||
|
// Remove completely if it was never saved
|
||||||
|
postTypesState = postTypesState.filter(pt => pt.id !== id);
|
||||||
|
} else {
|
||||||
|
// Mark as deleted
|
||||||
|
postType._status = 'deleted';
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPostTypesList();
|
||||||
|
checkForChanges();
|
||||||
|
showToast('Post-Typ gelöscht (noch nicht gespeichert)', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update strategy weight in state
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all changes to database
|
||||||
|
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('Du musst mindestens einen Post-Typ behalten!', '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/employee/post-types/${pt.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/employee/post-types', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
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/employee/post-types/${pt.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: pt.name,
|
||||||
|
description: pt.description,
|
||||||
|
strategy_weight: pt.strategy_weight
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Trigger save-all with structural changes flag
|
||||||
|
const saveAllResponse = await fetch('/api/employee/post-types/save-all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ has_structural_changes: hasStructuralChanges })
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveAllResult = await saveAllResponse.json();
|
||||||
|
|
||||||
|
if (saveAllResult.success) {
|
||||||
|
if (saveAllResult.recategorized) {
|
||||||
|
showToast('Änderungen gespeichert - Rekategorisierung läuft...', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('Änderungen erfolgreich gespeichert', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload page to get fresh data
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
throw new Error(saveAllResult.error || 'Save failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving changes:', error);
|
||||||
|
showToast('Fehler beim Speichern: ' + error.message, 'error');
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initializeState();
|
||||||
|
|
||||||
|
// Warn before leaving with unsaved changes
|
||||||
|
window.addEventListener('beforeunload', (e) => {
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,10 +10,19 @@
|
|||||||
<h1 class="text-2xl font-bold text-white">Post-Typen</h1>
|
<h1 class="text-2xl font-bold text-white">Post-Typen</h1>
|
||||||
<p class="text-gray-400 mt-1">Verwalte und kategorisiere deine LinkedIn-Posts</p>
|
<p class="text-gray-400 mt-1">Verwalte und kategorisiere deine LinkedIn-Posts</p>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="runAnalysis()" id="analyze-btn" class="btn-primary px-5 py-2.5 rounded-lg font-medium flex items-center gap-2">
|
<div class="flex gap-3">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
<a href="/post-types/manage" class="px-5 py-2.5 rounded-lg font-medium flex items-center gap-2 bg-brand-bg-dark border border-brand-bg-light text-white hover:bg-brand-bg-light transition-colors">
|
||||||
Analyse starten
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</button>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
</svg>
|
||||||
|
Post-Typen verwalten
|
||||||
|
</a>
|
||||||
|
<button onclick="runAnalysis()" id="analyze-btn" 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="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>
|
||||||
|
Analyse starten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter & Search Bar -->
|
<!-- Filter & Search Bar -->
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ from src.services.email_service import (
|
|||||||
from src.services.background_jobs import (
|
from src.services.background_jobs import (
|
||||||
job_manager, JobType, JobStatus,
|
job_manager, JobType, JobStatus,
|
||||||
run_post_scraping, run_profile_analysis, run_post_categorization, run_post_type_analysis,
|
run_post_scraping, run_profile_analysis, run_post_categorization, run_post_type_analysis,
|
||||||
run_full_analysis_pipeline
|
run_full_analysis_pipeline, run_post_recategorization
|
||||||
)
|
)
|
||||||
from src.services.storage_service import storage
|
from src.services.storage_service import storage
|
||||||
|
|
||||||
@@ -2948,6 +2948,310 @@ async def employee_strategy_page(request: Request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# EMPLOYEE POST TYPES MANAGEMENT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@user_router.get("/post-types/manage")
|
||||||
|
async def employee_post_types_page(request: Request):
|
||||||
|
"""Employee post types management page with strategy weight configuration."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
if session.account_type != "employee":
|
||||||
|
raise HTTPException(status_code=403, detail="Only employees can access this page")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
|
||||||
|
# Get all active post types for this employee
|
||||||
|
post_types = await db.get_post_types(user_id, active_only=True)
|
||||||
|
|
||||||
|
# Count posts for each post type and convert to JSON-serializable format
|
||||||
|
post_types_with_counts = []
|
||||||
|
for pt in post_types:
|
||||||
|
posts = await db.get_posts_by_type(user_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)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check if company strategy exists
|
||||||
|
company_strategy = None
|
||||||
|
has_strategy = False
|
||||||
|
if session.company_id:
|
||||||
|
company_id = UUID(session.company_id)
|
||||||
|
company = await db.get_company(company_id)
|
||||||
|
if company and company.company_strategy:
|
||||||
|
company_strategy = company.company_strategy
|
||||||
|
has_strategy = True
|
||||||
|
|
||||||
|
profile_picture = await get_user_avatar(session, user_id)
|
||||||
|
|
||||||
|
# Convert to JSON string for JavaScript
|
||||||
|
import json
|
||||||
|
post_types_json = json.dumps(post_types_with_counts)
|
||||||
|
|
||||||
|
logger.info(f"Generated JSON for {len(post_types_with_counts)} post types")
|
||||||
|
logger.info(f"JSON length: {len(post_types_json)} characters")
|
||||||
|
logger.info(f"JSON preview: {post_types_json[:200] if len(post_types_json) > 200 else post_types_json}")
|
||||||
|
|
||||||
|
return templates.TemplateResponse("employee_post_types.html", {
|
||||||
|
"request": request,
|
||||||
|
"page": "post_types",
|
||||||
|
"session": session,
|
||||||
|
"post_types_with_counts": post_types_with_counts,
|
||||||
|
"post_types_json": post_types_json,
|
||||||
|
"has_strategy": has_strategy,
|
||||||
|
"company_strategy": company_strategy,
|
||||||
|
"profile_picture": profile_picture
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading employee post types page: {e}")
|
||||||
|
profile_picture = await get_user_avatar(session, UUID(session.user_id))
|
||||||
|
import json
|
||||||
|
return templates.TemplateResponse("employee_post_types.html", {
|
||||||
|
"request": request,
|
||||||
|
"page": "post_types",
|
||||||
|
"session": session,
|
||||||
|
"post_types_with_counts": [],
|
||||||
|
"post_types_json": json.dumps([]),
|
||||||
|
"has_strategy": False,
|
||||||
|
"company_strategy": None,
|
||||||
|
"error": str(e),
|
||||||
|
"profile_picture": profile_picture
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.post("/api/employee/post-types")
|
||||||
|
async def create_employee_post_type(request: Request, background_tasks: BackgroundTasks):
|
||||||
|
"""Create a new 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 != "employee":
|
||||||
|
return JSONResponse({"error": "Only employees can create post types"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
|
||||||
|
# Validate required fields
|
||||||
|
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))
|
||||||
|
|
||||||
|
# Validate strategy_weight range
|
||||||
|
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 if an inactive post type with this name already exists
|
||||||
|
existing_inactive = None
|
||||||
|
try:
|
||||||
|
all_post_types = await db.get_post_types(user_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)
|
||||||
|
except Exception as check_error:
|
||||||
|
logger.warning(f"Could not check for inactive post types: {check_error}")
|
||||||
|
|
||||||
|
if existing_inactive:
|
||||||
|
# Reactivate the existing post type instead of creating a new one
|
||||||
|
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)
|
||||||
|
logger.info(f"Reactivated post type '{name}' for user {user_id}")
|
||||||
|
else:
|
||||||
|
# Create new post type
|
||||||
|
from src.database.models import PostType
|
||||||
|
post_type = PostType(
|
||||||
|
user_id=user_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)
|
||||||
|
logger.info(f"Created post type '{name}' for user {user_id}")
|
||||||
|
|
||||||
|
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 post type: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.put("/api/employee/post-types/{post_type_id}")
|
||||||
|
async def update_employee_post_type(request: Request, post_type_id: str):
|
||||||
|
"""Update an existing post type."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
|
||||||
|
if session.account_type != "employee":
|
||||||
|
return JSONResponse({"error": "Only employees can update post types"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
pt_id = UUID(post_type_id)
|
||||||
|
|
||||||
|
# Check ownership
|
||||||
|
post_type = await db.get_post_type(pt_id)
|
||||||
|
if not post_type or post_type.user_id != user_id:
|
||||||
|
return JSONResponse({"error": "Post type not found or access denied"}, status_code=404)
|
||||||
|
|
||||||
|
data = await request.json()
|
||||||
|
updates = {}
|
||||||
|
|
||||||
|
# Update name if provided
|
||||||
|
if "name" in data:
|
||||||
|
name_raw = data["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)
|
||||||
|
updates["name"] = name
|
||||||
|
|
||||||
|
# Update description if provided
|
||||||
|
if "description" in data:
|
||||||
|
desc_raw = data["description"]
|
||||||
|
updates["description"] = desc_raw.strip() if desc_raw else None
|
||||||
|
|
||||||
|
# Update strategy_weight if provided
|
||||||
|
if "strategy_weight" in data:
|
||||||
|
strategy_weight = float(data["strategy_weight"])
|
||||||
|
if not (0.0 <= strategy_weight <= 1.0):
|
||||||
|
return JSONResponse({"error": "Strategy weight must be between 0.0 and 1.0"}, status_code=400)
|
||||||
|
updates["strategy_weight"] = strategy_weight
|
||||||
|
|
||||||
|
# Apply updates
|
||||||
|
if updates:
|
||||||
|
await db.update_post_type(pt_id, updates)
|
||||||
|
|
||||||
|
return JSONResponse({"success": True})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating post type: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.delete("/api/employee/post-types/{post_type_id}")
|
||||||
|
async def delete_employee_post_type(request: Request, post_type_id: str, background_tasks: BackgroundTasks):
|
||||||
|
"""Soft delete a post type (set is_active = False)."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
|
||||||
|
if session.account_type != "employee":
|
||||||
|
return JSONResponse({"error": "Only employees can delete post types"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
pt_id = UUID(post_type_id)
|
||||||
|
|
||||||
|
# Check ownership
|
||||||
|
post_type = await db.get_post_type(pt_id)
|
||||||
|
if not post_type or post_type.user_id != user_id:
|
||||||
|
return JSONResponse({"error": "Post type not found or access denied"}, status_code=404)
|
||||||
|
|
||||||
|
# Count affected posts
|
||||||
|
posts = await db.get_posts_by_type(user_id, pt_id)
|
||||||
|
affected_count = len(posts)
|
||||||
|
|
||||||
|
# Soft delete
|
||||||
|
await db.update_post_type(pt_id, {"is_active": False})
|
||||||
|
|
||||||
|
logger.info(f"Deleted post type '{post_type.name}' for user {user_id}")
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"affected_posts": affected_count
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting post type: {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."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
|
||||||
|
if session.account_type != "employee":
|
||||||
|
return JSONResponse({"error": "Only employees can trigger re-analysis"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
has_structural_changes = data.get("has_structural_changes", False)
|
||||||
|
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
user_id_str = str(user_id)
|
||||||
|
|
||||||
|
# Only trigger re-categorization and analysis if there were structural changes
|
||||||
|
if has_structural_changes:
|
||||||
|
# Create background job for post re-categorization (ALL posts)
|
||||||
|
categorization_job = job_manager.create_job(
|
||||||
|
job_type=JobType.POST_CATEGORIZATION,
|
||||||
|
user_id=user_id_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create background job for post type analysis
|
||||||
|
analysis_job = job_manager.create_job(
|
||||||
|
job_type=JobType.POST_TYPE_ANALYSIS,
|
||||||
|
user_id=user_id_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start background tasks
|
||||||
|
background_tasks.add_task(run_post_recategorization, user_id, categorization_job.id)
|
||||||
|
background_tasks.add_task(run_post_type_analysis, user_id, analysis_job.id)
|
||||||
|
|
||||||
|
logger.info(f"Started re-analysis jobs for user {user_id}: categorization={categorization_job.id}, analysis={analysis_job.id}")
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"recategorized": True,
|
||||||
|
"categorization_job_id": str(categorization_job.id),
|
||||||
|
"analysis_job_id": str(analysis_job.id)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.info(f"No structural changes for user {user_id}, skipping re-analysis")
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"recategorized": False,
|
||||||
|
"message": "Nur Weight-Updates, keine Rekategorisierung notwendig"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in save-all: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
@user_router.post("/api/company/invite")
|
@user_router.post("/api/company/invite")
|
||||||
async def send_company_invitation(request: Request):
|
async def send_company_invitation(request: Request):
|
||||||
"""Send invitation to a new employee."""
|
"""Send invitation to a new employee."""
|
||||||
|
|||||||
Reference in New Issue
Block a user