added manual post add
This commit is contained in:
@@ -84,8 +84,6 @@ class DatabaseClient:
|
|||||||
|
|
||||||
async def save_linkedin_posts(self, posts: List[LinkedInPost]) -> List[LinkedInPost]:
|
async def save_linkedin_posts(self, posts: List[LinkedInPost]) -> List[LinkedInPost]:
|
||||||
"""Save LinkedIn posts (bulk)."""
|
"""Save LinkedIn posts (bulk)."""
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_posts = []
|
unique_posts = []
|
||||||
for p in posts:
|
for p in posts:
|
||||||
@@ -99,11 +97,9 @@ class DatabaseClient:
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
for p in unique_posts:
|
for p in unique_posts:
|
||||||
post_dict = p.model_dump(exclude={"id", "scraped_at"}, exclude_none=True)
|
# Use JSON mode so UUIDs/datetimes are serialized before the Supabase client
|
||||||
if "user_id" in post_dict:
|
# builds its request payload.
|
||||||
post_dict["user_id"] = str(post_dict["user_id"])
|
post_dict = p.model_dump(mode="json", exclude={"id", "scraped_at"}, exclude_none=True)
|
||||||
if "post_date" in post_dict and isinstance(post_dict["post_date"], datetime):
|
|
||||||
post_dict["post_date"] = post_dict["post_date"].isoformat()
|
|
||||||
data.append(post_dict)
|
data.append(post_dict)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@@ -176,6 +172,15 @@ class DatabaseClient:
|
|||||||
await cache.invalidate_linkedin_posts(user_id)
|
await cache.invalidate_linkedin_posts(user_id)
|
||||||
logger.info(f"Deleted LinkedIn post: {post_id}")
|
logger.info(f"Deleted LinkedIn post: {post_id}")
|
||||||
|
|
||||||
|
async def get_linkedin_post(self, post_id: UUID) -> Optional[LinkedInPost]:
|
||||||
|
"""Get a single LinkedIn post by ID."""
|
||||||
|
result = await asyncio.to_thread(
|
||||||
|
lambda: self.client.table("linkedin_posts").select("*").eq("id", str(post_id)).limit(1).execute()
|
||||||
|
)
|
||||||
|
if not result.data:
|
||||||
|
return None
|
||||||
|
return LinkedInPost(**result.data[0])
|
||||||
|
|
||||||
async def get_unclassified_posts(self, user_id: UUID) -> List[LinkedInPost]:
|
async def get_unclassified_posts(self, user_id: UUID) -> List[LinkedInPost]:
|
||||||
"""Get all LinkedIn posts without a post_type_id."""
|
"""Get all LinkedIn posts without a post_type_id."""
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
|
|||||||
@@ -187,6 +187,40 @@ function showToast(message, type = 'info') {
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Posts Modal -->
|
||||||
|
<div id="posts-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-4xl 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="posts-modal-title">Posts verwalten</h2>
|
||||||
|
<button onclick="closePostsModal()" 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>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-3">Vorhandene Posts</h3>
|
||||||
|
<div id="posts-modal-list" class="space-y-3"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-3">Post manuell hinzufügen</h3>
|
||||||
|
<form onsubmit="addManualPostToType(event)" class="space-y-4">
|
||||||
|
<input type="hidden" id="manual-post-type-id" value="">
|
||||||
|
<textarea id="manual-post-text" rows="10"
|
||||||
|
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
|
||||||
|
placeholder="Füge hier einen bestehenden Beispielpost ein. Er wird direkt diesem Post-Typ zugeordnet."></textarea>
|
||||||
|
<p class="text-sm text-gray-400">Dieser Post wird gespeichert und bei einer manuell gestarteten Re-Analyse mit berücksichtigt.</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button type="button" onclick="closePostsModal()" class="px-5 py-2.5 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors">Schließen</button>
|
||||||
|
<button type="submit" class="btn-primary px-5 py-2.5 rounded-lg font-medium">Post hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Modal (Wizard-Style) -->
|
<!-- 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 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="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">
|
||||||
@@ -422,6 +456,11 @@ function renderPostTypesList() {
|
|||||||
${pt.description ? `<p class="text-gray-400 text-sm">${escapeHtml(pt.description)}</p>` : ''}
|
${pt.description ? `<p class="text-gray-400 text-sm">${escapeHtml(pt.description)}</p>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<button onclick="openPostsModal('${pt.id}')"
|
||||||
|
${String(pt.id).startsWith('temp_') ? 'disabled' : ''}
|
||||||
|
class="px-3 py-2 text-sm text-brand-highlight hover:text-yellow-300 hover:bg-brand-highlight/10 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed">
|
||||||
|
Posts
|
||||||
|
</button>
|
||||||
<button onclick="editPostTypeState('${pt.id}')"
|
<button onclick="editPostTypeState('${pt.id}')"
|
||||||
class="px-3 py-2 text-sm text-blue-400 hover:text-blue-300 hover:bg-blue-900/20 rounded-lg transition-colors">
|
class="px-3 py-2 text-sm text-blue-400 hover:text-blue-300 hover:bg-blue-900/20 rounded-lg transition-colors">
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
@@ -553,6 +592,120 @@ function updateStrategyWeightState(id, value) {
|
|||||||
checkForChanges();
|
checkForChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentPostsModalPostTypeId = null;
|
||||||
|
let currentPostsModalPostTypeName = '';
|
||||||
|
|
||||||
|
async function openPostsModal(postTypeId) {
|
||||||
|
if (String(postTypeId).startsWith('temp_')) {
|
||||||
|
showToast('Bitte zuerst speichern, bevor du manuelle Posts verwaltest.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === postTypeId);
|
||||||
|
currentPostsModalPostTypeId = postTypeId;
|
||||||
|
currentPostsModalPostTypeName = postType ? postType.name : 'Post-Typ';
|
||||||
|
|
||||||
|
document.getElementById('posts-modal-title').textContent = `Posts in ${currentPostsModalPostTypeName}`;
|
||||||
|
document.getElementById('manual-post-type-id').value = postTypeId;
|
||||||
|
document.getElementById('manual-post-text').value = '';
|
||||||
|
document.getElementById('posts-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('posts-modal').classList.add('flex');
|
||||||
|
|
||||||
|
await loadPostsForModal(postTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePostsModal() {
|
||||||
|
document.getElementById('posts-modal').classList.add('hidden');
|
||||||
|
document.getElementById('posts-modal').classList.remove('flex');
|
||||||
|
currentPostsModalPostTypeId = null;
|
||||||
|
currentPostsModalPostTypeName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPostsForModal(postTypeId) {
|
||||||
|
const container = document.getElementById('posts-modal-list');
|
||||||
|
container.innerHTML = '<p class="text-gray-400">Lade Posts...</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/company/manage/post-types/${postTypeId}/posts?employee_id=${EMPLOYEE_ID}`);
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Posts konnten nicht geladen werden');
|
||||||
|
|
||||||
|
const posts = result.posts || [];
|
||||||
|
if (!posts.length) {
|
||||||
|
container.innerHTML = '<p class="text-gray-400">Noch keine Posts in diesem Post-Typ.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = posts.map(post => `
|
||||||
|
<div class="border border-brand-bg-light rounded-lg p-4 bg-brand-bg/30">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-3">
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
${post.post_date ? new Date(post.post_date).toLocaleString('de-DE') : 'Kein Datum'}
|
||||||
|
${post.classification_method ? ` · ${escapeHtml(post.classification_method)}` : ''}
|
||||||
|
</div>
|
||||||
|
<button onclick="deletePostFromModal('${post.id}')"
|
||||||
|
class="px-3 py-1.5 text-xs text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded-lg transition-colors">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-200 whitespace-pre-wrap">${escapeHtml(post.post_text)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (error) {
|
||||||
|
container.innerHTML = `<p class="text-red-400">Fehler: ${escapeHtml(error.message)}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addManualPostToType(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const postTypeId = document.getElementById('manual-post-type-id').value;
|
||||||
|
const postText = document.getElementById('manual-post-text').value.trim();
|
||||||
|
|
||||||
|
if (postText.length < 20) {
|
||||||
|
showToast('Bitte einen aussagekräftigen Post mit mindestens 20 Zeichen eingeben.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/company/manage/post-types/posts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ employee_id: EMPLOYEE_ID, post_type_id: postTypeId, post_text: postText })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Post konnte nicht hinzugefügt werden');
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === postTypeId);
|
||||||
|
if (postType) postType.post_count = (postType.post_count || 0) + 1;
|
||||||
|
renderPostTypesList();
|
||||||
|
document.getElementById('manual-post-text').value = '';
|
||||||
|
await loadPostsForModal(postTypeId);
|
||||||
|
showToast('Post hinzugefügt. Er wird bei der nächsten manuellen Re-Analyse mit berücksichtigt.', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showToast(`Fehler beim Hinzufügen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePostFromModal(postId) {
|
||||||
|
if (!currentPostsModalPostTypeId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/company/manage/post-types/${currentPostsModalPostTypeId}/posts/${postId}?employee_id=${EMPLOYEE_ID}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Post konnte nicht gelöscht werden');
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === currentPostsModalPostTypeId);
|
||||||
|
if (postType && postType.post_count > 0) postType.post_count -= 1;
|
||||||
|
renderPostTypesList();
|
||||||
|
await loadPostsForModal(currentPostsModalPostTypeId);
|
||||||
|
showToast('Post gelöscht.', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showToast(`Fehler beim Löschen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveAllChanges() {
|
async function saveAllChanges() {
|
||||||
const saveBtn = document.getElementById('save-all-btn');
|
const saveBtn = document.getElementById('save-all-btn');
|
||||||
if (!saveBtn) return;
|
if (!saveBtn) return;
|
||||||
|
|||||||
@@ -224,6 +224,40 @@ function showToast(message, type = 'info') {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Posts Modal -->
|
||||||
|
<div id="posts-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-4xl 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="posts-modal-title">Posts verwalten</h2>
|
||||||
|
<button onclick="closePostsModal()" 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>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-3">Vorhandene Posts</h3>
|
||||||
|
<div id="posts-modal-list" class="space-y-3"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-3">Post manuell hinzufügen</h3>
|
||||||
|
<form onsubmit="addManualPostToType(event)" class="space-y-4">
|
||||||
|
<input type="hidden" id="manual-post-type-id" value="">
|
||||||
|
<textarea id="manual-post-text" rows="10"
|
||||||
|
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
|
||||||
|
placeholder="Füge hier einen bestehenden Beispielpost ein. Er wird direkt diesem Post-Typ zugeordnet."></textarea>
|
||||||
|
<p class="text-sm text-gray-400">Dieser Post wird gespeichert und bei einer manuell gestarteten Re-Analyse mit berücksichtigt.</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button type="button" onclick="closePostsModal()" class="px-5 py-2.5 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors">Schließen</button>
|
||||||
|
<button type="submit" class="btn-primary px-5 py-2.5 rounded-lg font-medium">Post hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Create/Edit Modal (Wizard-Style) -->
|
<!-- 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 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="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">
|
||||||
@@ -568,6 +602,15 @@ function renderPostTypesList() {
|
|||||||
${pt.description ? `<p class="text-gray-400 text-sm">${escapeHtml(pt.description)}</p>` : ''}
|
${pt.description ? `<p class="text-gray-400 text-sm">${escapeHtml(pt.description)}</p>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<button onclick="openPostsModal('${pt.id}')"
|
||||||
|
${String(pt.id).startsWith('temp_') ? 'disabled' : ''}
|
||||||
|
class="px-3 py-2 text-sm text-brand-highlight hover:text-yellow-300 hover:bg-brand-highlight/10 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed">
|
||||||
|
Posts
|
||||||
|
</button>
|
||||||
|
<button onclick="editPostTypeState('${pt.id}')"
|
||||||
|
class="px-3 py-2 text-sm text-blue-400 hover:text-blue-300 hover:bg-blue-900/20 rounded-lg transition-colors">
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
<button onclick="deletePostTypeState('${pt.id}')"
|
<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">
|
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
|
Löschen
|
||||||
@@ -748,6 +791,120 @@ function updateStrategyWeightState(id, value) {
|
|||||||
checkForChanges();
|
checkForChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentPostsModalPostTypeId = null;
|
||||||
|
let currentPostsModalPostTypeName = '';
|
||||||
|
|
||||||
|
async function openPostsModal(postTypeId) {
|
||||||
|
if (String(postTypeId).startsWith('temp_')) {
|
||||||
|
showToast('Bitte zuerst speichern, bevor du manuelle Posts verwaltest.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === postTypeId);
|
||||||
|
currentPostsModalPostTypeId = postTypeId;
|
||||||
|
currentPostsModalPostTypeName = postType ? postType.name : 'Post-Typ';
|
||||||
|
|
||||||
|
document.getElementById('posts-modal-title').textContent = `Posts in ${currentPostsModalPostTypeName}`;
|
||||||
|
document.getElementById('manual-post-type-id').value = postTypeId;
|
||||||
|
document.getElementById('manual-post-text').value = '';
|
||||||
|
document.getElementById('posts-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('posts-modal').classList.add('flex');
|
||||||
|
|
||||||
|
await loadPostsForModal(postTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePostsModal() {
|
||||||
|
document.getElementById('posts-modal').classList.add('hidden');
|
||||||
|
document.getElementById('posts-modal').classList.remove('flex');
|
||||||
|
currentPostsModalPostTypeId = null;
|
||||||
|
currentPostsModalPostTypeName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPostsForModal(postTypeId) {
|
||||||
|
const container = document.getElementById('posts-modal-list');
|
||||||
|
container.innerHTML = '<p class="text-gray-400">Lade Posts...</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/employee/post-types/${postTypeId}/posts`);
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Posts konnten nicht geladen werden');
|
||||||
|
|
||||||
|
const posts = result.posts || [];
|
||||||
|
if (!posts.length) {
|
||||||
|
container.innerHTML = '<p class="text-gray-400">Noch keine Posts in diesem Post-Typ.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = posts.map(post => `
|
||||||
|
<div class="border border-brand-bg-light rounded-lg p-4 bg-brand-bg/30">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-3">
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
${post.post_date ? new Date(post.post_date).toLocaleString('de-DE') : 'Kein Datum'}
|
||||||
|
${post.classification_method ? ` · ${escapeHtml(post.classification_method)}` : ''}
|
||||||
|
</div>
|
||||||
|
<button onclick="deletePostFromModal('${post.id}')"
|
||||||
|
class="px-3 py-1.5 text-xs text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded-lg transition-colors">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-200 whitespace-pre-wrap">${escapeHtml(post.post_text)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (error) {
|
||||||
|
container.innerHTML = `<p class="text-red-400">Fehler: ${escapeHtml(error.message)}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addManualPostToType(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const postTypeId = document.getElementById('manual-post-type-id').value;
|
||||||
|
const postText = document.getElementById('manual-post-text').value.trim();
|
||||||
|
|
||||||
|
if (postText.length < 20) {
|
||||||
|
showToast('Bitte einen aussagekräftigen Post mit mindestens 20 Zeichen eingeben.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/employee/post-types/posts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ post_type_id: postTypeId, post_text: postText })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Post konnte nicht hinzugefügt werden');
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === postTypeId);
|
||||||
|
if (postType) postType.post_count = (postType.post_count || 0) + 1;
|
||||||
|
renderPostTypesList();
|
||||||
|
document.getElementById('manual-post-text').value = '';
|
||||||
|
await loadPostsForModal(postTypeId);
|
||||||
|
showToast('Post hinzugefügt. Er wird bei der nächsten manuellen Re-Analyse mit berücksichtigt.', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showToast(`Fehler beim Hinzufügen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePostFromModal(postId) {
|
||||||
|
if (!currentPostsModalPostTypeId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/employee/post-types/${currentPostsModalPostTypeId}/posts/${postId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Post konnte nicht gelöscht werden');
|
||||||
|
|
||||||
|
const postType = postTypesState.find(pt => pt.id === currentPostsModalPostTypeId);
|
||||||
|
if (postType && postType.post_count > 0) postType.post_count -= 1;
|
||||||
|
renderPostTypesList();
|
||||||
|
await loadPostsForModal(currentPostsModalPostTypeId);
|
||||||
|
showToast('Post gelöscht.', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showToast(`Fehler beim Löschen: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save all changes to database
|
// Save all changes to database
|
||||||
async function saveAllChanges() {
|
async function saveAllChanges() {
|
||||||
const saveBtn = document.getElementById('save-all-btn');
|
const saveBtn = document.getElementById('save-all-btn');
|
||||||
|
|||||||
@@ -22,6 +22,12 @@
|
|||||||
<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>
|
<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
|
Analyse starten
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick="openManualPostModal()" class="px-5 py-2.5 rounded-lg font-medium flex items-center gap-2 bg-brand-highlight text-brand-bg-dark hover:bg-brand-highlight-dark transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||||
|
</svg>
|
||||||
|
Post manuell hinzufügen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,6 +106,10 @@
|
|||||||
<option value="{{ pt.id }}">{{ pt.name }}</option>
|
<option value="{{ pt.id }}">{{ pt.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<button onclick="deletePost('{{ post.id }}')"
|
||||||
|
class="mt-2 w-full px-3 py-2 text-sm text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded-lg transition-colors">
|
||||||
|
Post löschen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -160,6 +170,10 @@
|
|||||||
<option value="{{ pt.id }}" {% if pt.id == post.post_type_id %}selected{% endif %}>{{ pt.name }}</option>
|
<option value="{{ pt.id }}" {% if pt.id == post.post_type_id %}selected{% endif %}>{{ pt.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<button onclick="deletePost('{{ post.id }}')"
|
||||||
|
class="mt-2 w-full px-3 py-2 text-sm text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded-lg transition-colors">
|
||||||
|
Post löschen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -207,7 +221,99 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Post Modal -->
|
||||||
|
<div id="manual-post-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">Post manuell hinzufügen</h2>
|
||||||
|
<button onclick="closeManualPostModal()" 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>
|
||||||
|
<form onsubmit="submitManualPost(event)" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Typ</label>
|
||||||
|
<select id="manual-post-type-id" class="w-full input-bg border rounded-lg px-4 py-3 text-white">
|
||||||
|
<option value="">Nicht kategorisiert</option>
|
||||||
|
{% for pt in post_types %}
|
||||||
|
<option value="{{ pt.id }}">{{ pt.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">Post-Text</label>
|
||||||
|
<textarea id="manual-post-text" rows="10"
|
||||||
|
class="w-full input-bg border rounded-lg px-4 py-3 text-white"
|
||||||
|
placeholder="Füge hier einen bestehenden Beispielpost ein. Er wird gespeichert und bei der nächsten manuellen Re-Analyse berücksichtigt."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button type="button" onclick="closeManualPostModal()" class="px-5 py-2.5 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-colors">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn-primary px-5 py-2.5 rounded-lg font-medium">Post speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function openManualPostModal() {
|
||||||
|
document.getElementById('manual-post-text').value = '';
|
||||||
|
document.getElementById('manual-post-type-id').value = '';
|
||||||
|
document.getElementById('manual-post-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('manual-post-modal').classList.add('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeManualPostModal() {
|
||||||
|
document.getElementById('manual-post-modal').classList.add('hidden');
|
||||||
|
document.getElementById('manual-post-modal').classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitManualPost(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const postText = document.getElementById('manual-post-text').value.trim();
|
||||||
|
const postTypeId = document.getElementById('manual-post-type-id').value;
|
||||||
|
|
||||||
|
if (postText.length < 20) {
|
||||||
|
if (window.showToast) showToast('Bitte einen Post mit mindestens 20 Zeichen eingeben.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/post-types/manual-posts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ post_text: postText, post_type_id: postTypeId || null })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Post konnte nicht hinzugefügt werden');
|
||||||
|
|
||||||
|
closeManualPostModal();
|
||||||
|
if (window.showToast) {
|
||||||
|
showToast('Post hinzugefügt. Er wird bei der nächsten manuellen Re-Analyse berücksichtigt.', 'success');
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
if (window.showToast) showToast(`Fehler: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePost(postId) {
|
||||||
|
if (!confirm('Diesen Post wirklich löschen?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/post-types/posts/${postId}`, { method: 'DELETE' });
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) throw new Error(result.error || 'Post konnte nicht gelöscht werden');
|
||||||
|
|
||||||
|
const postEl = document.querySelector(`[data-post-id="${postId}"]`);
|
||||||
|
if (postEl) postEl.remove();
|
||||||
|
if (window.showToast) showToast('Post gelöscht.', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
if (window.showToast) showToast(`Fehler: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleExpand(btn) {
|
function toggleExpand(btn) {
|
||||||
const card = btn.closest('.post-item');
|
const card = btn.closest('.post-item');
|
||||||
const fullContent = card.querySelector('.full-content');
|
const fullContent = card.querySelector('.full-content');
|
||||||
|
|||||||
@@ -1581,6 +1581,78 @@ async def api_categorize_post(request: Request):
|
|||||||
return JSONResponse({"error": str(e)}, status_code=500)
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.post("/api/post-types/manual-posts")
|
||||||
|
async def api_add_manual_post_for_post_types(request: Request):
|
||||||
|
"""Add a manual post from the post types page and optionally assign a type immediately."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
post_text = (data.get("post_text") or "").strip()
|
||||||
|
post_type_id_raw = data.get("post_type_id")
|
||||||
|
|
||||||
|
if len(post_text) < 20:
|
||||||
|
return JSONResponse({"error": "Post muss mindestens 20 Zeichen lang sein"}, status_code=400)
|
||||||
|
|
||||||
|
post_type_id = UUID(post_type_id_raw) if post_type_id_raw else None
|
||||||
|
if post_type_id:
|
||||||
|
post_type = await db.get_post_type(post_type_id)
|
||||||
|
if not post_type or post_type.user_id != user_id:
|
||||||
|
return JSONResponse({"error": "Post-Typ nicht gefunden oder kein Zugriff"}, status_code=404)
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
from src.database.models import LinkedInPost
|
||||||
|
|
||||||
|
manual_post = LinkedInPost(
|
||||||
|
user_id=user_id,
|
||||||
|
post_text=post_text,
|
||||||
|
post_url=f"manual://{uuid4()}",
|
||||||
|
post_date=datetime.now(timezone.utc),
|
||||||
|
post_type_id=post_type_id,
|
||||||
|
classification_method="manual",
|
||||||
|
classification_confidence=1.0,
|
||||||
|
raw_data={"source": "manual", "manual_entry": True}
|
||||||
|
)
|
||||||
|
saved_posts = await db.save_linkedin_posts([manual_post])
|
||||||
|
saved = saved_posts[0] if saved_posts else None
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"post": {
|
||||||
|
"id": str(saved.id) if saved and saved.id else None,
|
||||||
|
"post_text": saved.post_text if saved else post_text,
|
||||||
|
"post_date": saved.post_date.isoformat() if saved and saved.post_date else None,
|
||||||
|
"post_type_id": str(saved.post_type_id) if saved and saved.post_type_id else None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding manual post from post types page: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.delete("/api/post-types/posts/{post_id}")
|
||||||
|
async def api_delete_post_from_post_types_page(request: Request, post_id: str):
|
||||||
|
"""Delete a post from the main post types page."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
post = await db.get_linkedin_post(UUID(post_id))
|
||||||
|
if not post or post.user_id != user_id:
|
||||||
|
return JSONResponse({"error": "Post nicht gefunden oder kein Zugriff"}, status_code=404)
|
||||||
|
|
||||||
|
await db.delete_linkedin_post(UUID(post_id))
|
||||||
|
return JSONResponse({"success": True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting post from post types page: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
@user_router.get("/api/job-updates")
|
@user_router.get("/api/job-updates")
|
||||||
async def job_updates_sse(request: Request):
|
async def job_updates_sse(request: Request):
|
||||||
"""Server-Sent Events endpoint for job updates (Redis pub/sub — works across workers)."""
|
"""Server-Sent Events endpoint for job updates (Redis pub/sub — works across workers)."""
|
||||||
@@ -4259,6 +4331,250 @@ async def company_save_all_employee_post_types(request: Request, background_task
|
|||||||
return JSONResponse({"error": str(e)}, status_code=500)
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.get("/api/employee/post-types/{post_type_id}/posts")
|
||||||
|
async def employee_list_post_type_posts(request: Request, post_type_id: str):
|
||||||
|
"""List posts assigned to a 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 access this endpoint"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
pt_id = UUID(post_type_id)
|
||||||
|
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)
|
||||||
|
|
||||||
|
posts = await db.get_posts_by_type(user_id, pt_id)
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"posts": [
|
||||||
|
{
|
||||||
|
"id": str(p.id),
|
||||||
|
"post_text": p.post_text,
|
||||||
|
"post_date": p.post_date.isoformat() if p.post_date else None,
|
||||||
|
"post_url": p.post_url,
|
||||||
|
"classification_method": p.classification_method,
|
||||||
|
}
|
||||||
|
for p in posts
|
||||||
|
]
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing employee post type posts: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.post("/api/employee/post-types/posts")
|
||||||
|
async def employee_add_manual_post_to_type(request: Request):
|
||||||
|
"""Add a manual post and assign it to a 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 add posts here"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
pt_id = UUID(data.get("post_type_id"))
|
||||||
|
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)
|
||||||
|
|
||||||
|
post_text = (data.get("post_text") or "").strip()
|
||||||
|
if len(post_text) < 20:
|
||||||
|
return JSONResponse({"error": "Post muss mindestens 20 Zeichen lang sein"}, status_code=400)
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
from src.database.models import LinkedInPost
|
||||||
|
|
||||||
|
manual_post = LinkedInPost(
|
||||||
|
user_id=user_id,
|
||||||
|
post_text=post_text,
|
||||||
|
post_url=f"manual://{uuid4()}",
|
||||||
|
post_date=datetime.now(timezone.utc),
|
||||||
|
post_type_id=pt_id,
|
||||||
|
classification_method="manual",
|
||||||
|
classification_confidence=1.0,
|
||||||
|
raw_data={"source": "manual", "manual_entry": True}
|
||||||
|
)
|
||||||
|
saved_posts = await db.save_linkedin_posts([manual_post])
|
||||||
|
saved = saved_posts[0] if saved_posts else None
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"post": {
|
||||||
|
"id": str(saved.id) if saved and saved.id else None,
|
||||||
|
"post_text": saved.post_text if saved else post_text,
|
||||||
|
"post_date": saved.post_date.isoformat() if saved and saved.post_date else None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding manual post for employee post type: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.delete("/api/employee/post-types/{post_type_id}/posts/{post_id}")
|
||||||
|
async def employee_delete_post_from_type(request: Request, post_type_id: str, post_id: str):
|
||||||
|
"""Delete a post assigned to a 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 delete posts here"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
pt_id = UUID(post_type_id)
|
||||||
|
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)
|
||||||
|
|
||||||
|
post = await db.get_linkedin_post(UUID(post_id))
|
||||||
|
if not post or post.user_id != user_id or post.post_type_id != pt_id:
|
||||||
|
return JSONResponse({"error": "Post not found or access denied"}, status_code=404)
|
||||||
|
|
||||||
|
await db.delete_linkedin_post(UUID(post_id))
|
||||||
|
return JSONResponse({"success": True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting employee post from type: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.get("/api/company/manage/post-types/{post_type_id}/posts")
|
||||||
|
async def company_list_employee_post_type_posts(request: Request, post_type_id: str, employee_id: str):
|
||||||
|
"""Company lists posts assigned to an employee's post type."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
if session.account_type != "company" or not session.company_id:
|
||||||
|
return JSONResponse({"error": "Only company accounts can access this endpoint"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
emp_id = UUID(employee_id)
|
||||||
|
emp_user = await db.get_user(emp_id)
|
||||||
|
if not emp_user or str(emp_user.company_id) != session.company_id:
|
||||||
|
return JSONResponse({"error": "Employee not found or not in company"}, status_code=403)
|
||||||
|
|
||||||
|
pt_id = UUID(post_type_id)
|
||||||
|
post_type = await db.get_post_type(pt_id)
|
||||||
|
if not post_type or post_type.user_id != emp_id:
|
||||||
|
return JSONResponse({"error": "Post type not found or access denied"}, status_code=404)
|
||||||
|
|
||||||
|
posts = await db.get_posts_by_type(emp_id, pt_id)
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"posts": [
|
||||||
|
{
|
||||||
|
"id": str(p.id),
|
||||||
|
"post_text": p.post_text,
|
||||||
|
"post_date": p.post_date.isoformat() if p.post_date else None,
|
||||||
|
"post_url": p.post_url,
|
||||||
|
"classification_method": p.classification_method,
|
||||||
|
}
|
||||||
|
for p in posts
|
||||||
|
]
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing company-managed employee post type posts: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.post("/api/company/manage/post-types/posts")
|
||||||
|
async def company_add_manual_post_to_employee_type(request: Request):
|
||||||
|
"""Company adds a manual post for an employee and assigns it to a post type."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
if session.account_type != "company" or not session.company_id:
|
||||||
|
return JSONResponse({"error": "Only company accounts can add posts here"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
emp_id = UUID(data.get("employee_id"))
|
||||||
|
emp_user = await db.get_user(emp_id)
|
||||||
|
if not emp_user or str(emp_user.company_id) != session.company_id:
|
||||||
|
return JSONResponse({"error": "Employee not found or not in company"}, status_code=403)
|
||||||
|
|
||||||
|
perms = await get_employee_permissions_or_default(emp_id, UUID(session.company_id))
|
||||||
|
if not perms.get("can_manage_post_types", True):
|
||||||
|
return JSONResponse({"error": "Keine Berechtigung"}, status_code=403)
|
||||||
|
|
||||||
|
pt_id = UUID(data.get("post_type_id"))
|
||||||
|
post_type = await db.get_post_type(pt_id)
|
||||||
|
if not post_type or post_type.user_id != emp_id:
|
||||||
|
return JSONResponse({"error": "Post type not found or access denied"}, status_code=404)
|
||||||
|
|
||||||
|
post_text = (data.get("post_text") or "").strip()
|
||||||
|
if len(post_text) < 20:
|
||||||
|
return JSONResponse({"error": "Post muss mindestens 20 Zeichen lang sein"}, status_code=400)
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
from src.database.models import LinkedInPost
|
||||||
|
|
||||||
|
manual_post = LinkedInPost(
|
||||||
|
user_id=emp_id,
|
||||||
|
post_text=post_text,
|
||||||
|
post_url=f"manual://{uuid4()}",
|
||||||
|
post_date=datetime.now(timezone.utc),
|
||||||
|
post_type_id=pt_id,
|
||||||
|
classification_method="manual",
|
||||||
|
classification_confidence=1.0,
|
||||||
|
raw_data={"source": "manual", "manual_entry": True}
|
||||||
|
)
|
||||||
|
saved_posts = await db.save_linkedin_posts([manual_post])
|
||||||
|
saved = saved_posts[0] if saved_posts else None
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"post": {
|
||||||
|
"id": str(saved.id) if saved and saved.id else None,
|
||||||
|
"post_text": saved.post_text if saved else post_text,
|
||||||
|
"post_date": saved.post_date.isoformat() if saved and saved.post_date else None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding manual post for company-managed employee post type: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.delete("/api/company/manage/post-types/{post_type_id}/posts/{post_id}")
|
||||||
|
async def company_delete_post_from_employee_type(request: Request, post_type_id: str, post_id: str, employee_id: str):
|
||||||
|
"""Company deletes a post assigned to an employee's post type."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
if session.account_type != "company" or not session.company_id:
|
||||||
|
return JSONResponse({"error": "Only company accounts can delete posts here"}, status_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
emp_id = UUID(employee_id)
|
||||||
|
emp_user = await db.get_user(emp_id)
|
||||||
|
if not emp_user or str(emp_user.company_id) != session.company_id:
|
||||||
|
return JSONResponse({"error": "Employee not found or not in company"}, status_code=403)
|
||||||
|
|
||||||
|
perms = await get_employee_permissions_or_default(emp_id, UUID(session.company_id))
|
||||||
|
if not perms.get("can_manage_post_types", True):
|
||||||
|
return JSONResponse({"error": "Keine Berechtigung"}, status_code=403)
|
||||||
|
|
||||||
|
pt_id = UUID(post_type_id)
|
||||||
|
post_type = await db.get_post_type(pt_id)
|
||||||
|
if not post_type or post_type.user_id != emp_id:
|
||||||
|
return JSONResponse({"error": "Post type not found or access denied"}, status_code=404)
|
||||||
|
|
||||||
|
post = await db.get_linkedin_post(UUID(post_id))
|
||||||
|
if not post or post.user_id != emp_id or post.post_type_id != pt_id:
|
||||||
|
return JSONResponse({"error": "Post not found or access denied"}, status_code=404)
|
||||||
|
|
||||||
|
await db.delete_linkedin_post(UUID(post_id))
|
||||||
|
return JSONResponse({"success": True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting company-managed employee post from type: {e}")
|
||||||
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
@user_router.post("/api/employee/post-types/save-all")
|
@user_router.post("/api/employee/post-types/save-all")
|
||||||
async def save_all_and_reanalyze(request: Request, background_tasks: BackgroundTasks):
|
async def save_all_and_reanalyze(request: Request, background_tasks: BackgroundTasks):
|
||||||
"""Save all changes and conditionally trigger re-categorization based on structural changes."""
|
"""Save all changes and conditionally trigger re-categorization based on structural changes."""
|
||||||
|
|||||||
Reference in New Issue
Block a user