diff --git a/src/database/client.py b/src/database/client.py index d29438c..a194d63 100644 --- a/src/database/client.py +++ b/src/database/client.py @@ -84,8 +84,6 @@ class DatabaseClient: async def save_linkedin_posts(self, posts: List[LinkedInPost]) -> List[LinkedInPost]: """Save LinkedIn posts (bulk).""" - from datetime import datetime - seen = set() unique_posts = [] for p in posts: @@ -99,11 +97,9 @@ class DatabaseClient: data = [] for p in unique_posts: - post_dict = p.model_dump(exclude={"id", "scraped_at"}, exclude_none=True) - if "user_id" in post_dict: - post_dict["user_id"] = str(post_dict["user_id"]) - if "post_date" in post_dict and isinstance(post_dict["post_date"], datetime): - post_dict["post_date"] = post_dict["post_date"].isoformat() + # Use JSON mode so UUIDs/datetimes are serialized before the Supabase client + # builds its request payload. + post_dict = p.model_dump(mode="json", exclude={"id", "scraped_at"}, exclude_none=True) data.append(post_dict) if not data: @@ -176,6 +172,15 @@ class DatabaseClient: await cache.invalidate_linkedin_posts(user_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]: """Get all LinkedIn posts without a post_type_id.""" result = await asyncio.to_thread( diff --git a/src/web/templates/user/company_manage_post_types.html b/src/web/templates/user/company_manage_post_types.html index 5c14f28..3f9a441 100644 --- a/src/web/templates/user/company_manage_post_types.html +++ b/src/web/templates/user/company_manage_post_types.html @@ -187,6 +187,40 @@ function showToast(message, type = 'info') { {% endif %} + + + + `).join(''); + } catch (error) { + container.innerHTML = `

Fehler: ${escapeHtml(error.message)}

`; + } +} + +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() { const saveBtn = document.getElementById('save-all-btn'); if (!saveBtn) return; diff --git a/src/web/templates/user/employee_post_types.html b/src/web/templates/user/employee_post_types.html index bbee6d7..5aa9452 100644 --- a/src/web/templates/user/employee_post_types.html +++ b/src/web/templates/user/employee_post_types.html @@ -224,6 +224,40 @@ function showToast(message, type = 'info') { {% endif %} + + + + `).join(''); + } catch (error) { + container.innerHTML = `

Fehler: ${escapeHtml(error.message)}

`; + } +} + +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 async function saveAllChanges() { const saveBtn = document.getElementById('save-all-btn'); diff --git a/src/web/templates/user/post_types.html b/src/web/templates/user/post_types.html index 2009650..01e14ce 100644 --- a/src/web/templates/user/post_types.html +++ b/src/web/templates/user/post_types.html @@ -22,6 +22,12 @@ Analyse starten + @@ -100,6 +106,10 @@ {% endfor %} + @@ -160,6 +170,10 @@ {% endfor %} + @@ -207,7 +221,99 @@ + + +