feat(forum): Add email notifications for replies + custom tooltips
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- Email notifications sent to topic subscribers when new reply posted - Auto-subscribe users when they reply to a topic - Custom CSS tooltip on "seen by" avatars (replaces native title) - GET /forum/<id>/unsubscribe endpoint for email unsubscribe links - Clean up ROADMAP.md (remove unimplemented priorities, add RBAC/Slack) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f2fc1b89ec
commit
5e77ede9fa
@ -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/<int:topic_id>/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():
|
||||
|
||||
425
docs/ROADMAP.md
425
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 | - | - | + |
|
||||
|
||||
@ -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 @@
|
||||
<div class="seen-by-avatars">
|
||||
{% for read in topic_readers[:20] %}
|
||||
<div class="reader-avatar"
|
||||
title="{{ read.user.name or read.user.email.split('@')[0] }}{% if current_user.is_authenticated and read.user.id == current_user.id %} (Ty){% endif %}"
|
||||
data-name="{{ read.user.name or read.user.email.split('@')[0] }}{% if current_user.is_authenticated and read.user.id == current_user.id %} (Ty){% endif %}"
|
||||
style="background: hsl({{ (read.user.id * 137) % 360 }}, 65%, 50%);">
|
||||
{{ (read.user.name or read.user.email)[0]|upper }}
|
||||
</div>
|
||||
@ -1213,7 +1238,7 @@
|
||||
<div class="seen-by-avatars">
|
||||
{% for read in reply.readers[:15] %}
|
||||
<div class="reader-avatar"
|
||||
title="{{ read.user.name or read.user.email.split('@')[0] }}{% if current_user.is_authenticated and read.user.id == current_user.id %} (Ty){% endif %}"
|
||||
data-name="{{ read.user.name or read.user.email.split('@')[0] }}{% if current_user.is_authenticated and read.user.id == current_user.id %} (Ty){% endif %}"
|
||||
style="background: hsl({{ (read.user.id * 137) % 360 }}, 65%, 50%);">
|
||||
{{ (read.user.name or read.user.email)[0]|upper }}
|
||||
</div>
|
||||
|
||||
@ -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"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{ font-family: 'Inter', Arial, sans-serif; line-height: 1.6; color: #1e293b; }}
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; background: #f8fafc; }}
|
||||
.header {{ background-color: #1e3a8a; background: linear-gradient(135deg, #1e40af, #1e3a8a); color: white; padding: 24px; text-align: center; border-radius: 8px 8px 0 0; }}
|
||||
.header h1 {{ margin: 0; font-size: 22px; font-weight: 700; }}
|
||||
.content {{ background: white; padding: 30px; border-radius: 0 0 8px 8px; }}
|
||||
.quote {{ background: #f1f5f9; border-left: 4px solid #2563eb; padding: 15px; margin: 20px 0; border-radius: 0 8px 8px 0; color: #475569; font-style: italic; }}
|
||||
.button {{ display: inline-block; padding: 14px 32px; background: #2563eb; color: white; text-decoration: none; border-radius: 8px; margin: 20px 0; font-weight: 600; }}
|
||||
.footer {{ text-align: center; padding: 20px; color: #94a3b8; font-size: 0.85em; }}
|
||||
.footer a {{ color: #94a3b8; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Nowa odpowiedz na forum</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Czesc {subscriber.get('name', '')}!</p>
|
||||
<p><strong>{replier_name}</strong> odpowiedzial w temacie, ktory obserwujesz:</p>
|
||||
<h3 style="color: #1e3a8a;">{topic_title}</h3>
|
||||
<div class="quote">{preview}</div>
|
||||
<p style="text-align: center;">
|
||||
<a href="{topic_url}" class="button">Zobacz odpowiedz</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Norda Biznes Partner - Platforma Networkingu</p>
|
||||
<p><a href="{unsubscribe_url}">Przestam obserwowac ten watek</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user