diff --git a/blueprints/forum/routes.py b/blueprints/forum/routes.py index 6123f13..b211603 100644 --- a/blueprints/forum/routes.py +++ b/blueprints/forum/routes.py @@ -23,7 +23,8 @@ from utils.notifications import ( create_forum_reaction_notification, create_forum_solution_notification, create_forum_report_notification, - parse_mentions_and_notify + parse_mentions_and_notify, + send_forum_reply_email ) # Constants @@ -348,17 +349,34 @@ def forum_reply(topic_id): topic.updated_at = datetime.now() db.commit() - # Send notifications to subscribers (except the replier) + # Auto-subscribe replier to topic (if not already subscribed) try: - subscriptions = db.query(ForumTopicSubscription).filter( + existing_sub = db.query(ForumTopicSubscription).filter( + ForumTopicSubscription.topic_id == topic_id, + ForumTopicSubscription.user_id == current_user.id + ).first() + if not existing_sub: + db.add(ForumTopicSubscription( + topic_id=topic_id, + user_id=current_user.id, + notify_email=True, + notify_app=True + )) + db.commit() + except Exception as e: + logger.warning(f"Failed to auto-subscribe replier: {e}") + + # Send in-app notifications to subscribers (except the replier) + replier_name = current_user.name or current_user.email.split('@')[0] + try: + app_subs = db.query(ForumTopicSubscription).filter( ForumTopicSubscription.topic_id == topic_id, ForumTopicSubscription.user_id != current_user.id, ForumTopicSubscription.notify_app == True ).all() - subscriber_ids = [s.user_id for s in subscriptions] + subscriber_ids = [s.user_id for s in app_subs] if subscriber_ids: - replier_name = current_user.name or current_user.email.split('@')[0] create_forum_reply_notification( topic_id=topic_id, topic_title=topic.title, @@ -369,6 +387,35 @@ def forum_reply(topic_id): except Exception as e: logger.warning(f"Failed to send reply notifications: {e}") + # Send email notifications to subscribers with notify_email=True + try: + email_subs = db.query(ForumTopicSubscription).filter( + ForumTopicSubscription.topic_id == topic_id, + ForumTopicSubscription.user_id != current_user.id, + ForumTopicSubscription.notify_email == True + ).all() + + if email_subs: + subscriber_emails = [] + for sub in email_subs: + user = db.query(User).filter(User.id == sub.user_id).first() + if user and user.email: + subscriber_emails.append({ + 'email': user.email, + 'name': user.name or user.email.split('@')[0] + }) + + if subscriber_emails: + send_forum_reply_email( + topic_id=topic_id, + topic_title=topic.title, + replier_name=replier_name, + reply_content=content, + subscriber_emails=subscriber_emails + ) + except Exception as e: + logger.warning(f"Failed to send email notifications: {e}") + # Parse @mentions and send notifications try: author_name = current_user.name or current_user.email.split('@')[0] @@ -1014,6 +1061,32 @@ def unsubscribe_from_topic(topic_id): db.close() +@bp.route('/forum//unsubscribe', methods=['GET']) +@login_required +def unsubscribe_from_email(topic_id): + """Unsubscribe from topic via email link (GET, requires login)""" + db = SessionLocal() + try: + subscription = db.query(ForumTopicSubscription).filter( + ForumTopicSubscription.topic_id == topic_id, + ForumTopicSubscription.user_id == current_user.id + ).first() + + topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first() + topic_title = topic.title if topic else f"#{topic_id}" + + if subscription: + db.delete(subscription) + db.commit() + flash(f'Przestales obserwowac watek: {topic_title}', 'success') + else: + flash('Nie obserwujesz tego watku.', 'info') + + return redirect(url_for('.forum_topic', topic_id=topic_id)) + finally: + db.close() + + @bp.route('/forum/report', methods=['POST']) @login_required def report_content(): diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0fa300e..1ea81c6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -9,357 +9,12 @@ Główne instrukcje znajdują się w [CLAUDE.md](../CLAUDE.md). | Funkcjonalność | Status | Data | |----------------|--------|------| -| Social Media Audit | ✅ Wdrożone | 2026-01-09 | -| News Monitoring | ✅ Wdrożone | 2025-12-29 | -| Katalog firm | ✅ Wdrożone | 2025-11-23 | -| Chat AI (NordaGPT) | ✅ Wdrożone | 2025-11-23 | - ---- - -## Priorytet 1: Social Media Integration (Posts/Events) - -**Status:** Planowane -**Cel:** Pobieranie postów i wydarzeń z Social Media firm - -### Źródła danych -- Facebook Pages firm członkowskich (posty, wydarzenia) -- LinkedIn Company Pages -- Google My Business (recenzje, posty) - -### Wymagania techniczne -- Facebook Graph API (wymaga App Review dla pages_read_engagement) -- LinkedIn Marketing API (wymaga partnera lub OAuth) -- Google Business Profile API - -### Typy zdarzeń do importu -- `social_post` - posty z social media -- `social_event` - wydarzenia z Facebooka -- `review` - nowe recenzje Google - ---- - -## Priorytet 2: News Monitoring (Google/Brave API) - -**Status:** ✅ Wdrożone (2025-12-29) - -### Źródła danych -- Wzmianki o firmach w mediach lokalnych/branżowych -- Artykuły prasowe -- Komunikaty branżowe - -### Typy zdarzeń -- `news_mention` - wzmianka w mediach -- `press_release` - komunikat prasowy -- `award` - nagroda/wyróżnienie - ---- - -## Priorytet 3: Zarząd i Wspólnicy (rejestr.io) - -**Status:** Planowane -**Cel:** Wyświetlanie osób powiązanych z firmą bezpośrednio na stronie profilu - -### Dane do pobrania z rejestr.io -- Zarząd (Prezes, Wiceprezes, Członkowie Zarządu) -- Prokurenci -- Wspólnicy z % udziałów -- Beneficjenci rzeczywiści -- Linki do profili osób (powiązania z innymi firmami) - -### Wymagania techniczne -- Tabela `company_people` (company_id, name, role, shares_percent, person_url) -- Scraper Playwright (już mamy bazę w `analyze_connections.py`) -- Sekcja w `company_detail.html` po "Informacje prawne i biznesowe" - -### Przykład wyświetlania -``` -👥 ZARZĄD I WSPÓLNICY -┌─────────────────────────────────────────┐ -│ 👔 Jan Kowalski - Prezes Zarządu │ -│ 👔 Anna Nowak - Członek Zarządu │ -│ 💼 Firma XYZ Sp. z o.o. - 60% udziałów │ -│ 💼 Jan Kowalski - 40% udziałów │ -└─────────────────────────────────────────┘ -``` - -### Korzyści -- Widoczne powiązania między firmami Norda Biznes -- Ułatwiony networking (kto zna kogo) -- Transparentność struktury właścicielskiej - ---- - -## Priorytet 4: System rekomendacji i zdjęć - -**Status:** Planowane -**Cel:** Umożliwienie firmom członkowskim wzajemnego polecania się oraz prezentacji zdjęć - -### Funkcje -- Rekomendacje między firmami (kto poleca kogo) -- Galeria zdjęć firmy (realizacje, zespół, biuro) -- Wyświetlanie na profilu firmy - ---- - -## Priorytet 5: Status członkostwa i płatności - -**Status:** Planowane -**Cel:** Śledzenie statusu członkostwa i składek miesięcznych - -### Funkcje -- Status członka (aktywny, zawieszony, były członek) -- Informacja o opłaconych składkach -- Historia płatności -- Przypomnienia o zaległościach (dla admina) - ---- - -## Priorytet 6: System powiadomień - -**Status:** Planowane -**Cel:** Powiadamianie użytkowników o istotnych zdarzeniach bez konieczności logowania -**Źródło:** Feedback od członków Norda Biznes (Janusz Masiak, Angelika Piechocka) - 2026-01-22 - -### Kanały powiadomień - -| Kanał | Zastosowanie | Technologia | Koszt | -|-------|--------------|-------------|-------| -| **Email** | Standardowe powiadomienia | SendGrid / Mailgun / SMTP | ~$15/mies (10k maili) | -| **SMS** | Pilne/ważne powiadomienia | SMSAPI.pl | ~0.07 zł/SMS | -| **Push** | Natychmiastowe (PWA) | Web Push API | Darmowe | - -### Typy powiadomień - -| Typ | Email | SMS | Push | Domyślnie | -|-----|:-----:|:---:|:----:|:---------:| -| Nowe wydarzenia Norda Biznes | ✅ | ⚪ | ✅ | Email+Push | -| Nowe aktualności i newsy | ✅ | ❌ | ✅ | Email | -| Wiadomości od innych firm | ✅ | ⚪ | ✅ | Email+Push | -| Przypomnienia o spotkaniach | ✅ | ✅ | ✅ | Wszystkie | -| Newsletter tygodniowy | ✅ | ❌ | ❌ | Wyłączone | - -**Legenda:** ✅ dostępne domyślnie, ⚪ dostępne opcjonalnie, ❌ niedostępne - -### Panel preferencji powiadomień - -Użytkownik w profilu będzie mógł: -- Włączyć/wyłączyć poszczególne typy powiadomień -- Wybrać preferowane kanały (email, SMS, push) -- Ustawić "ciche godziny" (np. brak SMS po 21:00) -- Zapisać numer telefonu do SMS - -### Wymagania techniczne - -```python -# Nowa tabela: notification_preferences -class NotificationPreference(Base): - __tablename__ = 'notification_preferences' - - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey('users.id'), unique=True) - - # Kanały - email_enabled = Column(Boolean, default=True) - sms_enabled = Column(Boolean, default=False) - push_enabled = Column(Boolean, default=True) - phone_number = Column(String(15)) # Format: +48XXXXXXXXX - - # Typy powiadomień - events_email = Column(Boolean, default=True) - events_sms = Column(Boolean, default=False) - news_email = Column(Boolean, default=True) - messages_email = Column(Boolean, default=True) - messages_push = Column(Boolean, default=True) - reminders_sms = Column(Boolean, default=True) - newsletter = Column(Boolean, default=False) - - # Ciche godziny - quiet_hours_start = Column(Time) # np. 21:00 - quiet_hours_end = Column(Time) # np. 08:00 - -# Nowa tabela: notification_log (audit) -class NotificationLog(Base): - __tablename__ = 'notification_log' - - id = Column(Integer, primary_key=True) - user_id = Column(Integer, ForeignKey('users.id')) - channel = Column(String(10)) # email, sms, push - type = Column(String(30)) # event, news, message, reminder - subject = Column(String(200)) - sent_at = Column(DateTime, default=datetime.utcnow) - status = Column(String(20)) # sent, failed, bounced -``` - -### Integracje - -- **Email:** Flask-Mail + SendGrid API (fallback: SMTP) -- **SMS:** SMSAPI.pl (polska firma, dobra dokumentacja) -- **Push:** Web Push API + pywebpush (wymaga PWA - Priorytet 7) - ---- - -## Priorytet 7: PWA (Progressive Web App) - -**Status:** Planowane -**Cel:** Umożliwienie "instalacji" NordaBiznes na telefonie jako aplikacji -**Źródło:** Feedback od Angeliki Piechockiej - 2026-01-22 - -### Korzyści PWA vs natywna aplikacja - -| Aspekt | PWA | Natywna aplikacja | -|--------|-----|-------------------| -| Koszt rozwoju | Niski | Wysoki (iOS + Android) | -| Czas wdrożenia | 2-4 tygodnie | 3-6 miesięcy | -| Aktualizacje | Natychmiastowe | Wymaga publikacji w Store | -| Instalacja | "Dodaj do ekranu" | App Store / Google Play | -| Push notifications | ✅ (Android, iOS 16.4+) | ✅ | -| Dostęp offline | Ograniczony | Pełny | -| Kamera/GPS | ✅ | ✅ | - -### Wymagania techniczne - -1. **manifest.json** - metadane aplikacji (nazwa, ikony, kolory) -2. **Service Worker** - cache, offline, push notifications -3. **HTTPS** - już mamy (Let's Encrypt) -4. **Ikony** - różne rozmiary (192x192, 512x512) - -### Przykład manifest.json - -```json -{ - "name": "NordaBiznes - Katalog Firm", - "short_name": "NordaBiznes", - "description": "Platforma networkingowa członków Norda Biznes", - "start_url": "/", - "display": "standalone", - "background_color": "#1a1a2e", - "theme_color": "#6c5ce7", - "icons": [ - { - "src": "/static/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/static/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -} -``` - -### Fazy wdrożenia - -1. **Faza 1:** Podstawowy PWA (manifest + service worker cache) -2. **Faza 2:** Push notifications (integracja z Priorytetem 6) -3. **Faza 3:** Offline mode (cache kluczowych stron) - -### Zależności - -- Push notifications w PWA wymagają Service Worker -- Priorytet 6 (System powiadomień) i Priorytet 7 (PWA) są powiązane -- Rekomendacja: wdrażać równolegle - ---- - -## Priorytet 8: System ogłoszeń i aktualności - -**Status:** Planowane -**Cel:** Umożliwienie komunikacji z członkami Norda Biznes - ogłoszenia, aktualności, ważne informacje -**Źródło:** Potrzeba rozesłania informacji o bazie noclegowej ARP dla elektrowni jądrowej - 2026-01-26 - -### Problem - -Obecnie brak możliwości: -- Publikowania ogłoszeń dla członków na stronie www -- Wysyłania masowych komunikatów do wszystkich członków -- Targetowania komunikatów (np. tylko branża noclegowa) - -### Funkcjonalności - -#### Panel admina (`/admin/announcements`) -- Tworzenie nowych ogłoszeń (tytuł, treść, obrazek, link) -- Ustawianie daty publikacji i wygaśnięcia -- Wybór grupy docelowej (wszyscy / wybrane kategorie firm) -- Podgląd przed publikacją -- Historia wysłanych ogłoszeń - -#### Strona publiczna (`/aktualnosci`) -- Lista aktualnych ogłoszeń -- Archiwum starszych ogłoszeń -- Filtrowanie po kategorii -- RSS feed (opcjonalnie) - -#### Integracja z powiadomieniami (Priorytet 6) -- Automatyczne wysyłanie email do członków przy nowym ogłoszeniu -- Push notification (PWA) -- Opcja "wyślij teraz" vs "tylko opublikuj na stronie" - -### Wymagania techniczne - -```python -# Nowa tabela: announcements -class Announcement(Base): - __tablename__ = 'announcements' - - id = Column(Integer, primary_key=True) - title = Column(String(200), nullable=False) - slug = Column(String(200), unique=True) - content = Column(Text, nullable=False) # Markdown lub HTML - excerpt = Column(String(500)) # Krótki opis do listy - image_url = Column(String(500)) - external_link = Column(String(500)) # Link do zewnętrznego źródła - - # Publikacja - status = Column(String(20), default='draft') # draft, published, archived - published_at = Column(DateTime) - expires_at = Column(DateTime) # Opcjonalna data wygaśnięcia - - # Targetowanie - target_audience = Column(String(50), default='all') # all, category:IT, category:Services - - # Powiadomienia - send_email = Column(Boolean, default=True) - send_push = Column(Boolean, default=True) - notification_sent_at = Column(DateTime) - - # Meta - created_by = Column(Integer, ForeignKey('users.id')) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, onupdate=datetime.utcnow) - view_count = Column(Integer, default=0) - -# Tabela śledzenia kto widział ogłoszenie -class AnnouncementView(Base): - __tablename__ = 'announcement_views' - - id = Column(Integer, primary_key=True) - announcement_id = Column(Integer, ForeignKey('announcements.id')) - user_id = Column(Integer, ForeignKey('users.id')) - viewed_at = Column(DateTime, default=datetime.utcnow) -``` - -### Przykłady użycia - -| Scenariusz | Target | Email | Push | -|------------|--------|:-----:|:----:| -| Baza noclegowa ARP (elektrownia) | Wszystkie firmy | ✅ | ✅ | -| Szkolenie IT | category:IT | ✅ | ❌ | -| Spotkanie networkingowe | Wszystkie firmy | ✅ | ✅ | -| Aktualizacja regulaminu | Wszystkie firmy | ✅ | ❌ | - -### Zależności - -- **Priorytet 6 (Powiadomienia)** - do wysyłania email/push -- **Priorytet 7 (PWA)** - do push notifications -- Może działać samodzielnie (tylko strona www) bez powiadomień - -### Fazy wdrożenia - -1. **Faza 1:** Strona `/aktualnosci` + panel admina (bez powiadomień) -2. **Faza 2:** Integracja z email (gdy Priorytet 6 gotowy) -3. **Faza 3:** Push notifications (gdy Priorytet 7 gotowy) +| Social Media Audit | Wdrożone | 2026-01-09 | +| News Monitoring | Wdrożone | 2025-12-29 | +| Katalog firm | Wdrożone | 2025-11-23 | +| Chat AI (NordaGPT) | Wdrożone | 2025-11-23 | +| RBAC (Role-Based Access Control) | Wdrożone | 2026-02-05 | +| Slack MCP Integration | Wdrożone | 2026-02-06 | --- @@ -414,63 +69,11 @@ Najwyższy poziom służy jako **kotwica** - sprawia że środkowy wydaje się a | Funkcja | Basic | Premium | Enterprise | |---------|:-----:|:-------:|:----------:| -| Katalog firm | ✅ | ✅ | ✅ | -| Profil firmy | ✅ | ✅ | ✅ | -| Forum | ❌ | ✅ | ✅ | -| Kalendarz wydarzeń | ❌ | ✅ | ✅ | -| Chat AI (NordaGPT) | ❌ | ✅ | ✅ | -| **Powiadomienia email** | ✅ | ✅ | ✅ | -| **Powiadomienia SMS** | ❌ | ✅ | ✅ | -| **Powiadomienia push (PWA)** | ❌ | ✅ | ✅ | -| **Aktualności i ogłoszenia** | ✅ | ✅ | ✅ | -| **Raporty podstawowe** | ❌ | ✅ | ✅ | -| **Raporty zaawansowane** | ❌ | ❌ | ✅ | -| Eksport danych (CSV/PDF) | ❌ | ❌ | ✅ | -| API dostęp | ❌ | ❌ | ✅ | -| Priorytetowe wsparcie | ❌ | ❌ | ✅ | - -### Implementacja techniczna (przyszłość) - -```python -# Model User - nowe pola -class User(Base): - # ... - subscription_tier = Column(String(20), default='basic') # basic, premium, enterprise - subscription_expires_at = Column(DateTime) - is_norda_member = Column(Boolean, default=False) # Członek Izby = specjalny status - -# Dekorator kontroli dostępu -def requires_tier(min_tier): - def decorator(f): - @wraps(f) - def wrapped(*args, **kwargs): - tiers = ['basic', 'premium', 'enterprise'] - user_tier_idx = tiers.index(current_user.subscription_tier) - required_idx = tiers.index(min_tier) - if user_tier_idx < required_idx: - flash(f'Ta funkcja wymaga konta {min_tier.title()}.', 'warning') - return redirect(url_for('pricing')) - return f(*args, **kwargs) - return wrapped - return decorator - -# Użycie -@app.route('/raporty/zaawansowane') -@login_required -@requires_tier('enterprise') -def advanced_reports(): - ... -``` - -### Raporty - podział według poziomu - -**Raporty podstawowe (Premium+):** -- Staż członkostwa w Izbie NORDA -- Pokrycie Social Media -- Struktura branżowa - -**Raporty zaawansowane (Enterprise only):** -- Ranking SEO -- Mapa lokalizacji -- Sieć rekomendacji -- Aktywność w wydarzeniach +| Katalog firm | + | + | + | +| Profil firmy | + | + | + | +| Forum | - | + | + | +| Kalendarz wydarzeń | - | + | + | +| Chat AI (NordaGPT) | - | + | + | +| Eksport danych (CSV/PDF) | - | - | + | +| API dostęp | - | - | + | +| Priorytetowe wsparcie | - | - | + | diff --git a/templates/forum/topic.html b/templates/forum/topic.html index 54cabef..60f68dc 100755 --- a/templates/forum/topic.html +++ b/templates/forum/topic.html @@ -809,6 +809,31 @@ font-size: 11px; font-weight: 600; cursor: default; + position: relative; + } + + .reader-avatar[data-name]::after { + content: attr(data-name); + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + transform: translateX(-50%); + background: var(--bg-primary, #1a1a2e); + color: var(--text-primary, #fff); + padding: 4px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 500; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s; + z-index: 10; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + } + + .reader-avatar[data-name]:hover::after { + opacity: 1; } .reader-avatar.more { @@ -1073,7 +1098,7 @@
{% for read in topic_readers[:20] %}
{{ (read.user.name or read.user.email)[0]|upper }}
@@ -1213,7 +1238,7 @@
{% for read in reply.readers[:15] %}
{{ (read.user.name or read.user.email)[0]|upper }}
diff --git a/utils/notifications.py b/utils/notifications.py index 4613f14..226049a 100644 --- a/utils/notifications.py +++ b/utils/notifications.py @@ -257,6 +257,98 @@ def create_forum_reply_notification(topic_id, topic_title, replier_name, reply_i return count +def send_forum_reply_email(topic_id, topic_title, replier_name, reply_content, subscriber_emails): + """ + Send email notifications to forum topic subscribers about a new reply. + + Args: + topic_id: ID of the forum topic + topic_title: Title of the topic + replier_name: Name of the user who replied + reply_content: First 200 chars of the reply content + subscriber_emails: List of dicts with 'email' and 'name' keys + """ + from email_service import send_email + + base_url = "https://nordabiznes.pl" + topic_url = f"{base_url}/forum/{topic_id}" + unsubscribe_url = f"{base_url}/forum/{topic_id}/unsubscribe" + + preview = reply_content[:200].strip() + if len(reply_content) > 200: + preview += "..." + + count = 0 + for subscriber in subscriber_emails: + subject = f"Nowa odpowiedz na forum: {topic_title[:60]}" + + body_text = f"""{replier_name} odpowiedzial w temacie: {topic_title} + +"{preview}" + +Zobacz pelna odpowiedz: {topic_url} + +--- +Aby przestac obserwowac ten watek: {unsubscribe_url} +Norda Biznes Partner - https://nordabiznes.pl +""" + + body_html = f""" + + + + + + +
+
+

Nowa odpowiedz na forum

+
+
+

Czesc {subscriber.get('name', '')}!

+

{replier_name} odpowiedzial w temacie, ktory obserwujesz:

+

{topic_title}

+
{preview}
+

+ Zobacz odpowiedz +

+
+ +
+ +""" + + try: + result = send_email( + to=[subscriber['email']], + subject=subject, + body_text=body_text, + body_html=body_html, + email_type='forum_notification', + recipient_name=subscriber.get('name', '') + ) + if result: + count += 1 + except Exception as e: + logger.error(f"Failed to send forum reply email to {subscriber['email']}: {e}") + + logger.info(f"Sent {count}/{len(subscriber_emails)} forum reply emails for topic {topic_id}") + return count + + def create_forum_reaction_notification(user_id, reactor_name, content_type, content_id, topic_id, emoji): """ Notify user when someone reacts to their content.