# PEJ Section Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Create a dedicated PEJ (nuclear energy) section on nordabiznes.pl that filters ZOPK data, shows Local Content company list, and reorganizes navigation. **Architecture:** Hybrid "lens" approach — PEJ consumes existing ZOPK data (news, milestones, companies, facts, entities) filtered by nuclear project IDs. No new database tables. New routes in existing `public`/`admin` blueprints. Navigation reorganized: "Projekty ▾" dropdown replaces standalone "Kaszubia", admin nav reduced from 13 to 10 items. **Tech Stack:** Flask 3.0, SQLAlchemy 2.0, Jinja2, PostgreSQL, existing ZOPK models **Spec:** `docs/superpowers/specs/2026-03-16-pej-section-design.md` --- ## File Structure | Action | File | Responsibility | |--------|------|----------------| | Create | `blueprints/public/routes_pej.py` | 3 public routes: index, local-content, news | | Create | `blueprints/admin/routes_pej.py` | 1 admin route: CSV export | | Create | `templates/pej/index.html` | PEJ landing page | | Create | `templates/pej/local_content.html` | Company list with filters | | Create | `templates/pej/news.html` | Nuclear news list | | Create | `blueprints/pej_constants.py` | Shared constants: NUCLEAR_PROJECT_SLUGS, LINK_TYPE_LABELS, get_nuclear_project_ids() | | Create | `tests/unit/test_pej_helpers.py` | Unit test for helper function | | Modify | `database.py:4690-4696` | Add `'pej'` to Announcement.CATEGORIES/LABELS | | Modify | `blueprints/public/__init__.py:13` | Import routes_pej | | Modify | `blueprints/admin/__init__.py:20-23` | Import routes_pej | | Modify | `blueprints/__init__.py` | Add endpoint aliases for pej_index, pej_local_content, pej_news | | Modify | `templates/base.html:1461-1504` | Reorganize main NAV | | Modify | `templates/base.html:1855` | Add "Narzędzia" to admin bar | --- ## Chunk 1: Backend (Routes, Data, Model) ### Task 1: Announcement category + helper function **Files:** - Modify: `database.py:4690-4696` - Create: `tests/unit/test_pej_helpers.py` - [ ] **Step 1: Add `pej` to Announcement.CATEGORIES** In `database.py`, line 4690, change: ```python CATEGORIES = ['internal', 'external', 'event', 'opportunity', 'partnership'] ``` to: ```python CATEGORIES = ['internal', 'external', 'event', 'opportunity', 'partnership', 'pej'] ``` In `database.py`, after the last entry in `CATEGORY_LABELS` dict (~line 4696), add: ```python 'pej': 'PEJ / Energetyka jądrowa', ``` - [ ] **Step 2: Verify syntax** Run: `python -m py_compile database.py` Expected: No output (clean compile) - [ ] **Step 3: Commit** ```bash git add database.py git commit -m "feat(pej): add 'pej' category to Announcement model" ``` --- ### Task 2: PEJ public routes **Files:** - Create: `blueprints/public/routes_pej.py` - Modify: `blueprints/public/__init__.py:13` - [ ] **Step 1: Create shared constants module** Create `blueprints/pej_constants.py` — shared between public and admin PEJ routes: ```python """Shared constants and helpers for PEJ section.""" from database import db_session, ZOPKProject # Explicit slug list — easy to extend with SMR projects later NUCLEAR_PROJECT_SLUGS = ['nuclear-plant'] LINK_TYPE_LABELS = { 'potential_supplier': 'Potencjalny dostawca', 'partner': 'Partner', 'investor': 'Inwestor', 'beneficiary': 'Beneficjent' } def get_nuclear_project_ids(): """Return IDs of nuclear projects from ZOPK.""" projects = db_session.query(ZOPKProject.id).filter( ZOPKProject.slug.in_(NUCLEAR_PROJECT_SLUGS), ZOPKProject.project_type == 'energy' ).all() return [p.id for p in projects] ``` - [ ] **Step 2: Create routes_pej.py** Create `blueprints/public/routes_pej.py`. Pattern follows `routes_zopk.py` — imports `bp` from package, uses `@bp.route`. **WAŻNE — nazwy pól modeli:** - `Company.category` to relacja (ORM object), NIE string → filtruj po `Company.category_id`, wyświetlaj `company.category.name` - `Company.address_city` (NIE `city`) - `Company.pkd_code` (NIE `pkd_main`) - `Company.services_offered` (NIE `services` — to relacja) - `Announcement.categories.contains(['pej'])` (NIE `.any('pej')`) ```python """PEJ (nuclear energy) section routes — filtered lens on ZOPK data.""" import math from flask import render_template, request, abort from flask_login import login_required from sqlalchemy import func from . import bp from database import ( db_session, ZOPKNews, ZOPKMilestone, ZOPKCompanyLink, Company, Announcement, Category ) from blueprints.pej_constants import get_nuclear_project_ids, LINK_TYPE_LABELS @bp.route('/pej') @login_required def pej_index(): """PEJ landing page — hero, stats, news, timeline, top companies, announcements.""" nuclear_ids = get_nuclear_project_ids() if not nuclear_ids: abort(404) # Stats companies_count = db_session.query(func.count(ZOPKCompanyLink.id)).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25 ).scalar() or 0 news_count = db_session.query(func.count(ZOPKNews.id)).filter( ZOPKNews.project_id.in_(nuclear_ids), ZOPKNews.status.in_(['approved', 'auto_approved']) ).scalar() or 0 milestones_count = db_session.query(func.count(ZOPKMilestone.id)).filter( ZOPKMilestone.category == 'nuclear' ).scalar() or 0 # Latest news (4) news = db_session.query(ZOPKNews).filter( ZOPKNews.project_id.in_(nuclear_ids), ZOPKNews.status.in_(['approved', 'auto_approved']) ).order_by(ZOPKNews.published_at.desc()).limit(4).all() # Nuclear milestones milestones = db_session.query(ZOPKMilestone).filter( ZOPKMilestone.category == 'nuclear' ).order_by(ZOPKMilestone.target_date.asc()).all() # Top 6 companies by relevance top_companies = db_session.query(ZOPKCompanyLink, Company).join( Company, ZOPKCompanyLink.company_id == Company.id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active' ).order_by(ZOPKCompanyLink.relevance_score.desc()).limit(6).all() # PEJ announcements announcements = db_session.query(Announcement).filter( Announcement.categories.contains(['pej']), Announcement.status == 'approved' ).order_by(Announcement.created_at.desc()).limit(3).all() return render_template('pej/index.html', companies_count=companies_count, news_count=news_count, milestones_count=milestones_count, news=news, milestones=milestones, top_companies=top_companies, announcements=announcements, link_type_labels=LINK_TYPE_LABELS ) @bp.route('/pej/local-content') @login_required def pej_local_content(): """Full list of Norda companies matched to nuclear projects.""" nuclear_ids = get_nuclear_project_ids() if not nuclear_ids: abort(404) page = request.args.get('page', 1, type=int) per_page = 20 category_filter = request.args.get('category', '', type=int) # category_id (int) link_type_filter = request.args.get('link_type', '') search_query = request.args.get('q', '') query = db_session.query(ZOPKCompanyLink, Company).join( Company, ZOPKCompanyLink.company_id == Company.id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active' ) if category_filter: query = query.filter(Company.category_id == category_filter) if link_type_filter: query = query.filter(ZOPKCompanyLink.link_type == link_type_filter) if search_query: query = query.filter(Company.name.ilike(f'%{search_query}%')) total = query.count() results = query.order_by( ZOPKCompanyLink.relevance_score.desc() ).offset((page - 1) * per_page).limit(per_page).all() # Get distinct categories for filter dropdown (as Category objects) category_ids = db_session.query(Company.category_id).join( ZOPKCompanyLink, Company.id == ZOPKCompanyLink.company_id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active', Company.category_id.isnot(None) ).distinct().all() category_ids = [c[0] for c in category_ids if c[0]] categories = db_session.query(Category).filter( Category.id.in_(category_ids) ).order_by(Category.name).all() if category_ids else [] link_types = db_session.query(ZOPKCompanyLink.link_type).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25 ).distinct().all() link_types = sorted([lt[0] for lt in link_types if lt[0]]) total_pages = math.ceil(total / per_page) if total > 0 else 1 return render_template('pej/local_content.html', results=results, total=total, page=page, per_page=per_page, total_pages=total_pages, categories=categories, link_types=link_types, link_type_labels=LINK_TYPE_LABELS, category_filter=category_filter, link_type_filter=link_type_filter, search_query=search_query ) @bp.route('/pej/aktualnosci') @login_required def pej_news(): """Nuclear news list with pagination.""" nuclear_ids = get_nuclear_project_ids() if not nuclear_ids: abort(404) page = request.args.get('page', 1, type=int) per_page = 20 query = db_session.query(ZOPKNews).filter( ZOPKNews.project_id.in_(nuclear_ids), ZOPKNews.status.in_(['approved', 'auto_approved']) ).order_by(ZOPKNews.published_at.desc()) total = query.count() news = query.offset((page - 1) * per_page).limit(per_page).all() total_pages = math.ceil(total / per_page) if total > 0 else 1 return render_template('pej/news.html', news=news, page=page, total=total, total_pages=total_pages ) ``` - [ ] **Step 3: Register routes in public blueprint** In `blueprints/public/__init__.py`, after line 13 (the `routes_zopk` import), add: ```python from . import routes_pej # noqa: E402, F401 ``` - [ ] **Step 4: Add endpoint aliases** In `blueprints/__init__.py`, find the `ENDPOINT_ALIASES` dict (or equivalent pattern used for `zopk_index`, `company_detail` etc.) and add: ```python 'pej_index': 'public.pej_index', 'pej_local_content': 'public.pej_local_content', 'pej_news': 'public.pej_news', ``` This allows templates to use `url_for('pej_index')` without blueprint prefix, consistent with existing patterns like `url_for('zopk_index')`. - [ ] **Step 5: Verify syntax** Run: `python -m py_compile blueprints/public/routes_pej.py && python -m py_compile blueprints/pej_constants.py` Expected: No output (clean compile) - [ ] **Step 6: Commit** ```bash git add blueprints/pej_constants.py blueprints/public/routes_pej.py blueprints/public/__init__.py blueprints/__init__.py git commit -m "feat(pej): add public routes — index, local-content, news" ``` --- ### Task 3: PEJ admin route — CSV export **Files:** - Create: `blueprints/admin/routes_pej.py` - Modify: `blueprints/admin/__init__.py` - [ ] **Step 1: Create admin routes_pej.py** ```python """PEJ admin routes — CSV export of companies for PEJ local content.""" import csv import io from datetime import date from flask import Response from flask_login import login_required, current_user from . import bp from database import db_session, ZOPKCompanyLink, Company from blueprints.pej_constants import get_nuclear_project_ids, LINK_TYPE_LABELS @bp.route('/pej/export') @login_required def pej_export_csv(): """Export PEJ-matched companies as CSV.""" if not current_user.can_access_admin_panel(): return "Brak uprawnień", 403 nuclear_ids = get_nuclear_project_ids() results = db_session.query(ZOPKCompanyLink, Company).join( Company, ZOPKCompanyLink.company_id == Company.id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active' ).order_by(ZOPKCompanyLink.relevance_score.desc()).all() output = io.StringIO() # UTF-8 BOM for Excel output.write('\ufeff') writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_ALL) writer.writerow([ 'Nazwa firmy', 'Email', 'Telefon', 'Branża', 'PKD (główny)', 'Usługi', 'Typ współpracy PEJ', 'Opis współpracy', 'Score', 'Miasto' ]) for link, company in results: writer.writerow([ company.name or '', company.email or '', company.phone or '', company.category.name if company.category else '', company.pkd_code or '', company.services_offered or '', LINK_TYPE_LABELS.get(link.link_type, link.link_type or ''), link.collaboration_description or '', link.relevance_score or 0, company.address_city or '' ]) filename = f'pej-local-content-{date.today().isoformat()}.csv' return Response( output.getvalue(), mimetype='text/csv; charset=utf-8', headers={'Content-Disposition': f'attachment; filename="{filename}"'} ) ``` - [ ] **Step 2: Register in admin blueprint** In `blueprints/admin/__init__.py`, after line 23 (the ZOPK imports block), add: ```python from . import routes_pej # noqa: E402, F401 ``` - [ ] **Step 3: Verify syntax** Run: `python -m py_compile blueprints/admin/routes_pej.py` Expected: No output (clean compile) - [ ] **Step 4: Commit** ```bash git add blueprints/admin/routes_pej.py blueprints/admin/__init__.py git commit -m "feat(pej): add admin CSV export route" ``` --- ## Chunk 2: Templates ### Task 4: PEJ landing page template **Files:** - Create: `templates/pej/index.html` - [ ] **Step 1: Create templates/pej/ directory** Run: `mkdir -p templates/pej` - [ ] **Step 2: Create index.html** Template extends `base.html`. Uses purple/indigo color scheme (#7c3aed / #4f46e5). Layout: 1. Hero section with title and description 2. Stats bar (companies, news, milestones counts) 3. Announcements section (PEJ category) 4. Two-column: news (left) + timeline (right) 5. Local Content preview (top 6 companies) ```html {% extends "base.html" %} {% block title %}PEJ — Elektrownia Jądrowa - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
Izba Norda Biznes aktywnie uczestniczy w projekcie PEJ. Tu znajdziesz aktualności, listę firm gotowych do współpracy i informacje o możliwościach dla członków.
{{ ann.content[:300] }}{% if ann.content|length > 300 %}...{% endif %}