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
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS (57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash commands, memory files, architecture docs, and deploy procedures. Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted 155 .strftime() calls across 71 templates so timestamps display in Polish timezone regardless of server timezone. Also includes: created_by_id tracking, abort import fix, ICS calendar fix for missing end times, Pros Poland data cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1145 lines
42 KiB
Markdown
1145 lines
42 KiB
Markdown
# 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 %}
|
|
<style>
|
|
.pej-hero {
|
|
background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);
|
|
color: #fff;
|
|
padding: var(--spacing-2xl) 0;
|
|
margin: calc(-1 * var(--spacing-lg)) calc(-1 * var(--spacing-lg)) var(--spacing-xl);
|
|
text-align: center;
|
|
}
|
|
.pej-hero h1 { font-size: 2rem; margin-bottom: var(--spacing-sm); }
|
|
.pej-hero p { opacity: 0.9; max-width: 600px; margin: 0 auto; font-size: var(--font-size-md); }
|
|
|
|
.pej-stats {
|
|
display: flex; justify-content: center; gap: var(--spacing-xl);
|
|
padding: var(--spacing-md) 0;
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
.pej-stat { text-align: center; }
|
|
.pej-stat-value { font-size: 2rem; font-weight: 700; color: #7c3aed; }
|
|
.pej-stat-label { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
|
|
|
.pej-section { margin-bottom: var(--spacing-xl); }
|
|
.pej-section-title {
|
|
font-size: var(--font-size-lg); font-weight: 600;
|
|
margin-bottom: var(--spacing-md);
|
|
display: flex; align-items: center; gap: var(--spacing-sm);
|
|
}
|
|
|
|
.pej-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-lg); }
|
|
@media (max-width: 768px) { .pej-grid { grid-template-columns: 1fr; } }
|
|
|
|
.pej-announcement {
|
|
background: #f5f3ff; border: 1px solid #e9d5ff; border-radius: var(--radius);
|
|
padding: var(--spacing-md); margin-bottom: var(--spacing-sm);
|
|
}
|
|
.pej-announcement-title { font-weight: 600; margin-bottom: var(--spacing-xs); }
|
|
.pej-announcement-date { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
|
|
|
.pej-news-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: var(--spacing-md);
|
|
margin-bottom: var(--spacing-sm); transition: var(--transition);
|
|
}
|
|
.pej-news-card:hover { border-color: #7c3aed; }
|
|
.pej-news-card h3 { font-size: var(--font-size-md); margin-bottom: var(--spacing-xs); }
|
|
.pej-news-card h3 a { color: var(--text-primary); text-decoration: none; }
|
|
.pej-news-card h3 a:hover { color: #7c3aed; }
|
|
.pej-news-card .meta { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
|
|
|
.pej-milestone {
|
|
display: flex; gap: var(--spacing-md); padding: var(--spacing-sm) 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.pej-milestone:last-child { border-bottom: none; }
|
|
.pej-milestone-date {
|
|
min-width: 80px; font-size: var(--font-size-sm); color: #7c3aed; font-weight: 600;
|
|
}
|
|
.pej-milestone-title { font-size: var(--font-size-sm); }
|
|
.pej-milestone-status {
|
|
font-size: 0.7rem; padding: 2px 6px; border-radius: 10px;
|
|
display: inline-block; margin-left: var(--spacing-xs);
|
|
}
|
|
.pej-milestone-status.planned { background: #e0e7ff; color: #3730a3; }
|
|
.pej-milestone-status.in_progress { background: #fef3c7; color: #92400e; }
|
|
.pej-milestone-status.completed { background: #d1fae5; color: #065f46; }
|
|
.pej-milestone-status.delayed { background: #fee2e2; color: #991b1b; }
|
|
|
|
.pej-company-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-md); }
|
|
@media (max-width: 768px) { .pej-company-grid { grid-template-columns: 1fr; } }
|
|
@media (min-width: 769px) and (max-width: 1024px) { .pej-company-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
|
|
.pej-company-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: var(--spacing-md);
|
|
transition: var(--transition);
|
|
}
|
|
.pej-company-card:hover { border-color: #7c3aed; box-shadow: 0 2px 8px rgba(124,58,237,0.1); }
|
|
.pej-company-name { font-weight: 600; margin-bottom: var(--spacing-xs); }
|
|
.pej-company-name a { color: var(--text-primary); text-decoration: none; }
|
|
.pej-company-name a:hover { color: #7c3aed; }
|
|
.pej-company-meta { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
|
.pej-company-score {
|
|
display: inline-block; background: #f5f3ff; color: #7c3aed;
|
|
padding: 2px 8px; border-radius: 10px; font-size: var(--font-size-sm); font-weight: 600;
|
|
}
|
|
.pej-company-type {
|
|
display: inline-block; background: #e0e7ff; color: #3730a3;
|
|
padding: 2px 8px; border-radius: 10px; font-size: 0.75rem;
|
|
}
|
|
.pej-company-desc {
|
|
font-size: var(--font-size-sm); color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.pej-cta {
|
|
text-align: center; margin-top: var(--spacing-lg);
|
|
display: flex; gap: var(--spacing-md); justify-content: center; flex-wrap: wrap;
|
|
}
|
|
.pej-btn {
|
|
display: inline-block; padding: var(--spacing-sm) var(--spacing-lg);
|
|
border-radius: var(--radius); text-decoration: none; font-weight: 500;
|
|
transition: var(--transition);
|
|
}
|
|
.pej-btn-primary { background: #7c3aed; color: #fff; }
|
|
.pej-btn-primary:hover { background: #6d28d9; }
|
|
.pej-btn-outline { border: 1px solid #7c3aed; color: #7c3aed; }
|
|
.pej-btn-outline:hover { background: #f5f3ff; }
|
|
|
|
.pej-empty { text-align: center; padding: var(--spacing-xl); color: var(--text-secondary); }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="pej-hero">
|
|
<h1>Elektrownia Jądrowa — Szanse dla Naszych Firm</h1>
|
|
<p>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.</p>
|
|
</div>
|
|
|
|
<div class="pej-stats">
|
|
<div class="pej-stat">
|
|
<div class="pej-stat-value">{{ companies_count }}</div>
|
|
<div class="pej-stat-label">firm gotowych</div>
|
|
</div>
|
|
<div class="pej-stat">
|
|
<div class="pej-stat-value">{{ news_count }}</div>
|
|
<div class="pej-stat-label">aktualności</div>
|
|
</div>
|
|
<div class="pej-stat">
|
|
<div class="pej-stat-value">{{ milestones_count }}</div>
|
|
<div class="pej-stat-label">kamieni milowych</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if announcements %}
|
|
<div class="pej-section">
|
|
<div class="pej-section-title">Komunikaty Izby</div>
|
|
{% for ann in announcements %}
|
|
<div class="pej-announcement">
|
|
<div class="pej-announcement-title">{{ ann.title }}</div>
|
|
<div class="pej-announcement-date">{{ ann.created_at.strftime('%d.%m.%Y') }}</div>
|
|
<p>{{ ann.content[:300] }}{% if ann.content|length > 300 %}...{% endif %}</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="pej-grid">
|
|
<div class="pej-section">
|
|
<div class="pej-section-title">Najnowsze aktualności</div>
|
|
{% if news %}
|
|
{% for item in news %}
|
|
<div class="pej-news-card">
|
|
<h3><a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.title }}</a></h3>
|
|
<div class="meta">
|
|
{{ item.source_name or '' }}
|
|
{% if item.published_at %} · {{ item.published_at.strftime('%d.%m.%Y') }}{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
<a href="{{ url_for('pej_news') }}" class="pej-btn pej-btn-outline" style="margin-top: var(--spacing-sm); display: inline-block;">Zobacz wszystkie →</a>
|
|
{% else %}
|
|
<div class="pej-empty">Brak aktualności nuklearnych.</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="pej-section">
|
|
<div class="pej-section-title">Kamienie milowe</div>
|
|
{% if milestones %}
|
|
{% for m in milestones %}
|
|
<div class="pej-milestone">
|
|
<div class="pej-milestone-date">
|
|
{% if m.target_date %}{{ m.target_date.strftime('%m/%Y') }}{% else %}—{% endif %}
|
|
</div>
|
|
<div>
|
|
<span class="pej-milestone-title">{{ m.title }}</span>
|
|
<span class="pej-milestone-status {{ m.status }}">{{ m.status }}</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="pej-empty">Brak kamieni milowych.</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pej-section">
|
|
<div class="pej-section-title">Local Content — Firmy z Izby gotowe do współpracy</div>
|
|
{% if top_companies %}
|
|
<div class="pej-company-grid">
|
|
{% for link, company in top_companies %}
|
|
<div class="pej-company-card">
|
|
<div class="pej-company-name">
|
|
<a href="{{ url_for('company_detail', slug=company.slug) }}">{{ company.name }}</a>
|
|
</div>
|
|
<div class="pej-company-meta">
|
|
{% if company.category %}<span>{{ company.category.name }}</span>{% endif %}
|
|
<span class="pej-company-score">{{ link.relevance_score }}/100</span>
|
|
{% if link.link_type %}
|
|
<span class="pej-company-type">
|
|
{% if link.link_type == 'potential_supplier' %}Dostawca
|
|
{% elif link.link_type == 'partner' %}Partner
|
|
{% elif link.link_type == 'investor' %}Inwestor
|
|
{% elif link.link_type == 'beneficiary' %}Beneficjent
|
|
{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if link.collaboration_description %}
|
|
<div class="pej-company-desc">{{ link.collaboration_description[:150] }}{% if link.collaboration_description|length > 150 %}...{% endif %}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="pej-cta">
|
|
<a href="{{ url_for('pej_local_content') }}" class="pej-btn pej-btn-primary">Zobacz pełną listę →</a>
|
|
{% if current_user.can_access_admin_panel() %}
|
|
<a href="{{ url_for('admin.pej_export_csv') }}" class="pej-btn pej-btn-outline">Eksportuj CSV</a>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="pej-empty">Brak dopasowanych firm.</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add templates/pej/index.html
|
|
git commit -m "feat(pej): add landing page template"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Local Content template
|
|
|
|
**Files:**
|
|
- Create: `templates/pej/local_content.html`
|
|
|
|
- [ ] **Step 1: Create local_content.html**
|
|
|
|
```html
|
|
{% extends "base.html" %}
|
|
|
|
{% block title %}Local Content — Firmy dla PEJ - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.lc-header {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
margin-bottom: var(--spacing-lg); flex-wrap: wrap; gap: var(--spacing-md);
|
|
}
|
|
.lc-header h1 { font-size: 1.5rem; color: #7c3aed; }
|
|
.lc-export-btn {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: #7c3aed; color: #fff; border-radius: var(--radius);
|
|
text-decoration: none; font-size: var(--font-size-sm); font-weight: 500;
|
|
}
|
|
.lc-export-btn:hover { background: #6d28d9; }
|
|
|
|
.lc-filters {
|
|
display: flex; gap: var(--spacing-md); margin-bottom: var(--spacing-lg);
|
|
flex-wrap: wrap; align-items: center;
|
|
}
|
|
.lc-filters select, .lc-filters input {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border); border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
.lc-filter-btn {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: #7c3aed; color: #fff; border: none;
|
|
border-radius: var(--radius); cursor: pointer; font-size: var(--font-size-sm);
|
|
}
|
|
.lc-filter-btn:hover { background: #6d28d9; }
|
|
.lc-count { font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: var(--spacing-md); }
|
|
|
|
.lc-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md); display: flex;
|
|
justify-content: space-between; align-items: flex-start;
|
|
gap: var(--spacing-md); transition: var(--transition);
|
|
}
|
|
.lc-card:hover { border-color: #7c3aed; }
|
|
.lc-card-main { flex: 1; }
|
|
.lc-card-name { font-weight: 600; font-size: var(--font-size-md); margin-bottom: var(--spacing-xs); }
|
|
.lc-card-name a { color: var(--text-primary); text-decoration: none; }
|
|
.lc-card-name a:hover { color: #7c3aed; }
|
|
.lc-card-meta { font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: var(--spacing-xs); }
|
|
.lc-card-desc { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
|
.lc-card-side { text-align: right; min-width: 100px; }
|
|
.lc-score {
|
|
font-size: 1.25rem; font-weight: 700; color: #7c3aed;
|
|
}
|
|
.lc-type {
|
|
display: inline-block; background: #e0e7ff; color: #3730a3;
|
|
padding: 2px 8px; border-radius: 10px; font-size: 0.75rem; margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.lc-pagination { display: flex; justify-content: center; gap: var(--spacing-sm); margin-top: var(--spacing-lg); }
|
|
.lc-pagination a {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border); border-radius: var(--radius);
|
|
text-decoration: none; color: var(--text-primary); font-size: var(--font-size-sm);
|
|
}
|
|
.lc-pagination a:hover { border-color: #7c3aed; color: #7c3aed; }
|
|
.lc-pagination a.active { background: #7c3aed; color: #fff; border-color: #7c3aed; }
|
|
|
|
@media (max-width: 768px) {
|
|
.lc-card { flex-direction: column; }
|
|
.lc-card-side { text-align: left; }
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="lc-header">
|
|
<div>
|
|
<h1>Local Content — Firmy Izby Norda dla PEJ</h1>
|
|
<a href="{{ url_for('pej_index') }}" style="font-size: var(--font-size-sm); color: var(--text-secondary);">← Powrót do PEJ</a>
|
|
</div>
|
|
{% if current_user.can_access_admin_panel() %}
|
|
<a href="{{ url_for('admin.pej_export_csv') }}" class="lc-export-btn">
|
|
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
Eksportuj CSV
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<form class="lc-filters" method="get">
|
|
<select name="category">
|
|
<option value="">Wszystkie branże</option>
|
|
{% for cat in categories %}
|
|
<option value="{{ cat.id }}" {% if cat.id == category_filter %}selected{% endif %}>{{ cat.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<select name="link_type">
|
|
<option value="">Wszystkie typy</option>
|
|
{% for lt in link_types %}
|
|
<option value="{{ lt }}" {% if lt == link_type_filter %}selected{% endif %}>{{ link_type_labels.get(lt, lt) }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input type="text" name="q" placeholder="Szukaj firmy..." value="{{ search_query }}">
|
|
<button type="submit" class="lc-filter-btn">Filtruj</button>
|
|
{% if category_filter or link_type_filter or search_query %}
|
|
<a href="{{ url_for('pej_local_content') }}" style="font-size: var(--font-size-sm); color: var(--text-secondary);">Wyczyść filtry</a>
|
|
{% endif %}
|
|
</form>
|
|
|
|
<div class="lc-count">Znaleziono {{ total }} firm</div>
|
|
|
|
{% for link, company in results %}
|
|
<div class="lc-card">
|
|
<div class="lc-card-main">
|
|
<div class="lc-card-name">
|
|
<a href="{{ url_for('company_detail', slug=company.slug) }}">{{ company.name }}</a>
|
|
</div>
|
|
<div class="lc-card-meta">
|
|
{% if company.category %}{{ company.category.name }}{% endif %}
|
|
{% if company.address_city %} · {{ company.address_city }}{% endif %}
|
|
{% if company.email %} · {{ company.email }}{% endif %}
|
|
</div>
|
|
{% if link.collaboration_description %}
|
|
<div class="lc-card-desc">{{ link.collaboration_description }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="lc-card-side">
|
|
<div class="lc-score">{{ link.relevance_score }}<span style="font-size: 0.7em; color: var(--text-secondary);">/100</span></div>
|
|
{% if link.link_type %}
|
|
<div class="lc-type">{{ link_type_labels.get(link.link_type, link.link_type) }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div style="text-align: center; padding: var(--spacing-xl); color: var(--text-secondary);">
|
|
Brak firm spełniających kryteria.
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% if total_pages > 1 %}
|
|
<div class="lc-pagination">
|
|
{% if page > 1 %}
|
|
<a href="?page={{ page - 1 }}&category={{ category_filter }}&link_type={{ link_type_filter }}&q={{ search_query }}">« Poprzednia</a>
|
|
{% endif %}
|
|
{% for p in range(1, total_pages + 1) %}
|
|
{% if p == page %}
|
|
<a class="active">{{ p }}</a>
|
|
{% elif p <= 3 or p > total_pages - 2 or (p >= page - 1 and p <= page + 1) %}
|
|
<a href="?page={{ p }}&category={{ category_filter }}&link_type={{ link_type_filter }}&q={{ search_query }}">{{ p }}</a>
|
|
{% elif p == 4 or p == total_pages - 2 %}
|
|
<span style="padding: var(--spacing-sm);">...</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if page < total_pages %}
|
|
<a href="?page={{ page + 1 }}&category={{ category_filter }}&link_type={{ link_type_filter }}&q={{ search_query }}">Nastepna »</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add templates/pej/local_content.html
|
|
git commit -m "feat(pej): add Local Content company list template"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: PEJ news template
|
|
|
|
**Files:**
|
|
- Create: `templates/pej/news.html`
|
|
|
|
- [ ] **Step 1: Create news.html**
|
|
|
|
Follows the same pattern as `zopk/news_list.html` but simplified (no project filter needed — all content is already nuclear).
|
|
|
|
```html
|
|
{% extends "base.html" %}
|
|
|
|
{% block title %}Aktualności PEJ - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.pej-news-header {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
.pej-news-header h1 { font-size: 1.5rem; color: #7c3aed; }
|
|
|
|
.pej-news-item {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md); transition: var(--transition);
|
|
}
|
|
.pej-news-item:hover { border-color: #7c3aed; }
|
|
.pej-news-item h2 { font-size: var(--font-size-md); margin-bottom: var(--spacing-xs); }
|
|
.pej-news-item h2 a { color: var(--text-primary); text-decoration: none; }
|
|
.pej-news-item h2 a:hover { color: #7c3aed; }
|
|
.pej-news-item .meta {
|
|
font-size: var(--font-size-sm); color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
.pej-news-item .summary { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
|
|
|
.pej-pagination { display: flex; justify-content: center; gap: var(--spacing-sm); margin-top: var(--spacing-lg); }
|
|
.pej-pagination a {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border); border-radius: var(--radius);
|
|
text-decoration: none; color: var(--text-primary); font-size: var(--font-size-sm);
|
|
}
|
|
.pej-pagination a:hover { border-color: #7c3aed; color: #7c3aed; }
|
|
.pej-pagination a.active { background: #7c3aed; color: #fff; border-color: #7c3aed; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="pej-news-header">
|
|
<h1>Aktualności — Elektrownia Jądrowa</h1>
|
|
<a href="{{ url_for('pej_index') }}" style="font-size: var(--font-size-sm); color: var(--text-secondary);">← Powrót do PEJ</a>
|
|
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin-top: var(--spacing-xs);">{{ total }} artykułów</p>
|
|
</div>
|
|
|
|
{% for item in news %}
|
|
<div class="pej-news-item">
|
|
<h2><a href="{{ item.url }}" target="_blank" rel="noopener">{{ item.title }}</a></h2>
|
|
<div class="meta">
|
|
{{ item.source_name or 'Źródło nieznane' }}
|
|
{% if item.published_at %} · {{ item.published_at.strftime('%d.%m.%Y') }}{% endif %}
|
|
{% if item.ai_relevance_score %}
|
|
· <span title="Trafność AI">{% for i in range(item.ai_relevance_score) %}★{% endfor %}{% for i in range(5 - item.ai_relevance_score) %}☆{% endfor %}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if item.summary %}
|
|
<div class="summary">{{ item.summary[:200] }}{% if item.summary|length > 200 %}...{% endif %}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div style="text-align: center; padding: var(--spacing-xl); color: var(--text-secondary);">
|
|
Brak aktualności nuklearnych.
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% if total_pages > 1 %}
|
|
<div class="pej-pagination">
|
|
{% if page > 1 %}
|
|
<a href="?page={{ page - 1 }}">« Poprzednia</a>
|
|
{% endif %}
|
|
{% for p in range(1, total_pages + 1) %}
|
|
{% if p == page %}
|
|
<a class="active">{{ p }}</a>
|
|
{% elif p <= 3 or p > total_pages - 2 or (p >= page - 1 and p <= page + 1) %}
|
|
<a href="?page={{ p }}">{{ p }}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if page < total_pages %}
|
|
<a href="?page={{ page + 1 }}">Następna »</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add templates/pej/news.html
|
|
git commit -m "feat(pej): add nuclear news list template"
|
|
```
|
|
|
|
---
|
|
|
|
## Chunk 3: Navigation Reorganization
|
|
|
|
### Task 7: Main NAV — "Projekty ▾" dropdown + cleanup
|
|
|
|
**Files:**
|
|
- Modify: `templates/base.html:1461-1504`
|
|
|
|
This is the most delicate change — modifying shared navigation. Changes:
|
|
|
|
1. Remove "Social (beta)" from main NAV (line 1461-1464) — already in admin bar
|
|
2. Remove "Korzyści" from main NAV (line 1487-1489) — already in admin bar
|
|
3. Replace the entire Kaszubia/Więcej block (lines 1491-1504) with a unified "Projekty ▾" dropdown for ALL users
|
|
|
|
- [ ] **Step 1: Remove "Social (beta)" from main NAV**
|
|
|
|
Delete lines 1461-1464 (the `{% if MANAGER %}` block with Social link):
|
|
```html
|
|
<!-- Remove this block: -->
|
|
{% if current_user.is_authenticated and current_user.has_role(SystemRole.MANAGER) %}
|
|
<li><a href="{{ url_for('admin.social_publisher_list') }}" class="nav-link {% if request.endpoint and 'social_publisher' in request.endpoint %}active{% endif %}">Social <span class="nav-badge-beta">beta</span></a></li>
|
|
{% endif %}
|
|
```
|
|
|
|
- [ ] **Step 2: Remove "Korzyści" from main NAV**
|
|
|
|
Delete lines 1487-1489 (the `{% if can_access_admin_panel %}` block with Korzyści):
|
|
```html
|
|
<!-- Remove this block: -->
|
|
{% if current_user.can_access_admin_panel() %}
|
|
<li><a href="{{ url_for('benefits.benefits_list') }}" class="nav-link {% if request.endpoint and 'benefits' in request.endpoint %}active{% endif %}">Korzyści</a></li>
|
|
{% endif %}
|
|
```
|
|
|
|
- [ ] **Step 3: Replace Kaszubia/Więcej with "Projekty ▾" dropdown**
|
|
|
|
Replace the entire block from lines 1491-1504 (the `{% if maciej %}` Więcej / `{% else %}` Kaszubia block) with:
|
|
|
|
```html
|
|
<!-- Projekty dropdown — for all logged-in users -->
|
|
<li class="nav-dropdown">
|
|
<a href="#" class="nav-link {% if request.endpoint and ('zopk' in (request.endpoint or '') or 'pej' in (request.endpoint or '')) %}active{% endif %}">Projekty ▾</a>
|
|
<ul class="nav-dropdown-menu">
|
|
<li><a href="{{ url_for('zopk_index') }}">Kaszubia</a></li>
|
|
<li><a href="{{ url_for('pej_index') }}">PEJ</a></li>
|
|
</ul>
|
|
</li>
|
|
```
|
|
|
|
- [ ] **Step 4: Verify the result**
|
|
|
|
Final admin main NAV should be 10 items:
|
|
```
|
|
Firmy | NordaGPT | Kalendarz | B2B | Forum | Wiadomości | Aktualności | Edukacja | Rada | Projekty▾
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add templates/base.html
|
|
git commit -m "refactor(nav): replace Kaszubia/Więcej with Projekty dropdown, remove duplicates"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Admin bar — "Narzędzia" dropdown
|
|
|
|
**Files:**
|
|
- Modify: `templates/base.html:1855`
|
|
|
|
Move Kontakty zewnętrzne, Raporty, Mapa Powiązań from main NAV (removed in Task 7) to admin bar.
|
|
|
|
- [ ] **Step 1: Add "Narzędzia" dropdown to admin bar**
|
|
|
|
Insert after line 1855 (closing `</div>` of "Treści" dropdown), before the admin bar container closing `</div>`:
|
|
|
|
```html
|
|
<!-- Narzędzia (owner-only: Kontakty zewnętrzne, Raporty, Mapa Powiązań) -->
|
|
{% if is_audit_owner %}
|
|
<div class="admin-dropdown">
|
|
<button class="admin-dropdown-trigger">
|
|
Narzędzia
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
<div class="admin-dropdown-menu">
|
|
<a href="{{ url_for('contacts.contacts_list') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
|
</svg>
|
|
Kontakty zewnętrzne
|
|
</a>
|
|
<a href="{{ url_for('reports.reports_index') }}">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
Raporty
|
|
</a>
|
|
<a href="#" onclick="openConnectionsMap(); return false;">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l5.447 2.724A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
|
</svg>
|
|
Mapa Powiązań
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add templates/base.html
|
|
git commit -m "refactor(nav): add Narzędzia dropdown to admin bar"
|
|
```
|
|
|
|
---
|
|
|
|
## Chunk 4: Verification
|
|
|
|
### Task 9: Local development testing
|
|
|
|
- [ ] **Step 1: Start the application**
|
|
|
|
Run: `python3 app.py`
|
|
Expected: Server starts on port 5000/5001 without import errors.
|
|
|
|
- [ ] **Step 2: Verify PEJ routes**
|
|
|
|
1. Navigate to `http://localhost:5000/pej` (logged in) — should show landing page with purple hero
|
|
2. Navigate to `http://localhost:5000/pej/local-content` — should show company list
|
|
3. Navigate to `http://localhost:5000/pej/aktualnosci` — should show nuclear news
|
|
4. Navigate to `http://localhost:5000/admin/pej/export` (as admin) — should download CSV
|
|
|
|
- [ ] **Step 3: Verify navigation**
|
|
|
|
1. As admin: main NAV has 10 items, "Projekty ▾" dropdown shows Kaszubia + PEJ
|
|
2. As admin: "Social" and "Korzyści" NOT in main NAV
|
|
3. As admin: admin bar has "Narzędzia" dropdown with Kontakty, Raporty, Mapa
|
|
4. As regular user: "Projekty ▾" dropdown shows Kaszubia + PEJ
|
|
|
|
- [ ] **Step 4: Mobile responsiveness**
|
|
|
|
Check all pages at 375px viewport width.
|
|
|
|
- [ ] **Step 5: Commit verification notes**
|
|
|
|
If any fixes were needed, commit them as separate fix commits.
|
|
|
|
---
|
|
|
|
### Task 10: Deploy to staging
|
|
|
|
Follow standard deployment procedure from CLAUDE.md:
|
|
|
|
- [ ] **Step 1: Push to both remotes**
|
|
|
|
```bash
|
|
git push origin master && git push inpi master
|
|
```
|
|
|
|
- [ ] **Step 2: Deploy to staging**
|
|
|
|
```bash
|
|
ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
|
```
|
|
|
|
- [ ] **Step 3: Verify on staging**
|
|
|
|
```bash
|
|
curl -sI https://staging.nordabiznes.pl/health | head -3
|
|
```
|
|
|
|
Then manually test `/pej`, `/pej/local-content`, `/pej/aktualnosci` on staging.
|
|
|
|
- [ ] **Step 4: Deploy to production (AFTER staging verification)**
|
|
|
|
```bash
|
|
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
|
curl -sI https://nordabiznes.pl/health | head -3
|
|
```
|