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>
1493 lines
50 KiB
HTML
1493 lines
50 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Zielony Okręg Przemysłowy Kaszubia - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.zopk-hero {
|
|
background: linear-gradient(135deg, #059669 0%, #047857 50%, #065f46 100%);
|
|
color: white;
|
|
padding: var(--spacing-2xl) 0;
|
|
margin-bottom: var(--spacing-xl);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.zopk-hero h1 {
|
|
font-size: var(--font-size-3xl);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.zopk-hero p {
|
|
font-size: var(--font-size-lg);
|
|
opacity: 0.9;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.zopk-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-top: var(--spacing-xl);
|
|
}
|
|
|
|
.zopk-stat {
|
|
text-align: center;
|
|
padding: var(--spacing-md);
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.zopk-stat-value {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.zopk-stat-label {
|
|
font-size: var(--font-size-sm);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.section-header h2 {
|
|
font-size: var(--font-size-xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Projects Grid */
|
|
.projects-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.project-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
box-shadow: var(--shadow);
|
|
transition: var(--transition);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
display: block;
|
|
border-left: 4px solid var(--primary);
|
|
}
|
|
|
|
.project-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.project-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.project-card h3 {
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.project-card p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.project-status {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
margin-top: var(--spacing-sm);
|
|
}
|
|
|
|
.status-planned {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.status-in_progress {
|
|
background: #dbeafe;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.status-completed {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
/* Stakeholders */
|
|
.stakeholders-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.stakeholder-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.stakeholder-avatar {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-lg);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.stakeholder-info h4 {
|
|
font-size: var(--font-size-base);
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.stakeholder-info p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.stakeholder-category {
|
|
font-size: var(--font-size-xs);
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
background: #f3f4f6;
|
|
color: #374151;
|
|
}
|
|
|
|
.category-government { background: #fee2e2; color: #991b1b; }
|
|
.category-local_authority { background: #dbeafe; color: #1e40af; }
|
|
.category-business { background: #dcfce7; color: #166534; }
|
|
|
|
/* News Statistics Bar */
|
|
.news-stats-bar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-lg);
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
|
border-radius: var(--radius);
|
|
border: 1px solid #86efac;
|
|
}
|
|
|
|
.news-stat-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.news-stat-item .stat-value {
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.news-stat-item .stat-label {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.news-stat-item:not(:last-child)::after {
|
|
content: "•";
|
|
margin-left: var(--spacing-lg);
|
|
color: var(--border);
|
|
}
|
|
|
|
/* News - matching /zopk/aktualnosci style */
|
|
.news-grid {
|
|
display: grid;
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.news-card {
|
|
display: grid;
|
|
grid-template-columns: 100px 1fr;
|
|
gap: var(--spacing-lg);
|
|
background: var(--surface);
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.news-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.news-image {
|
|
width: 100px;
|
|
height: 80px;
|
|
border-radius: var(--radius);
|
|
object-fit: cover;
|
|
background: var(--background);
|
|
}
|
|
|
|
.news-placeholder {
|
|
width: 100px;
|
|
height: 80px;
|
|
border-radius: var(--radius);
|
|
background: linear-gradient(135deg, #059669, #047857);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.news-source-initial {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
opacity: 0.8;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.news-content h4 {
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: var(--spacing-sm);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.news-content p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
line-height: 1.6;
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.news-meta {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Resources */
|
|
.resources-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.resource-card {
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: var(--transition);
|
|
text-align: center;
|
|
}
|
|
|
|
.resource-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.resource-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin: 0 auto var(--spacing-sm);
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.resource-card h4 {
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.resource-type {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-2xl);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Timeline / Roadmapa */
|
|
.timeline {
|
|
position: relative;
|
|
padding: 0 0 0 40px;
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 15px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: linear-gradient(180deg, var(--primary) 0%, #10b981 50%, #6366f1 100%);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
padding-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.timeline-item:last-child {
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.timeline-icon {
|
|
position: absolute;
|
|
left: -40px;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
z-index: 1;
|
|
box-shadow: 0 0 0 4px var(--background);
|
|
}
|
|
|
|
.timeline-icon.status-completed {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.timeline-icon.status-in_progress {
|
|
background: #3b82f6;
|
|
color: white;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.timeline-icon.status-planned {
|
|
background: #f59e0b;
|
|
color: white;
|
|
}
|
|
|
|
.timeline-icon.status-delayed {
|
|
background: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { box-shadow: 0 0 0 4px var(--background); }
|
|
50% { box-shadow: 0 0 0 8px rgba(59, 130, 246, 0.2); }
|
|
}
|
|
|
|
.timeline-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
box-shadow: var(--shadow);
|
|
border-left: 4px solid var(--primary);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.timeline-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.timeline-card.featured {
|
|
border-left-width: 6px;
|
|
background: linear-gradient(135deg, var(--surface) 0%, #f0fdf4 100%);
|
|
}
|
|
|
|
.timeline-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.timeline-card h4 {
|
|
font-size: var(--font-size-lg);
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
.timeline-date {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
white-space: nowrap;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.timeline-date svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.timeline-card p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
}
|
|
|
|
.timeline-meta {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
margin-top: var(--spacing-sm);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.timeline-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge-completed { background: #dcfce7; color: #166534; }
|
|
.badge-in_progress { background: #dbeafe; color: #1e40af; }
|
|
.badge-planned { background: #fef3c7; color: #92400e; }
|
|
.badge-delayed { background: #fee2e2; color: #991b1b; }
|
|
|
|
.timeline-type {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.timeline-source {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.timeline-source:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.timeline-year-marker {
|
|
position: relative;
|
|
padding: var(--spacing-sm) 0 var(--spacing-lg) 0;
|
|
}
|
|
|
|
.timeline-year-marker::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -40px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 32px;
|
|
height: 32px;
|
|
background: var(--primary);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 0 0 4px var(--background);
|
|
}
|
|
|
|
.timeline-year {
|
|
display: inline-block;
|
|
background: var(--primary);
|
|
color: white;
|
|
padding: 4px 12px;
|
|
border-radius: var(--radius);
|
|
font-weight: 700;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.zopk-hero {
|
|
padding: var(--spacing-xl) var(--spacing-md);
|
|
}
|
|
|
|
.zopk-hero h1 {
|
|
font-size: var(--font-size-2xl);
|
|
}
|
|
|
|
.projects-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.news-card {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.news-image,
|
|
.news-placeholder {
|
|
width: 100%;
|
|
height: 150px;
|
|
}
|
|
|
|
.timeline {
|
|
padding-left: 30px;
|
|
}
|
|
|
|
.timeline::before {
|
|
left: 10px;
|
|
}
|
|
|
|
.timeline-icon {
|
|
left: -30px;
|
|
width: 24px;
|
|
height: 24px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.timeline-header {
|
|
flex-direction: column;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.timeline-card h4 {
|
|
font-size: var(--font-size-base);
|
|
}
|
|
}
|
|
|
|
/* ====== Knowledge Section (admin only) ====== */
|
|
.knowledge-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.knowledge-stat-box {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
box-shadow: var(--shadow);
|
|
text-align: center;
|
|
}
|
|
|
|
.knowledge-stat-box .stat-number {
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.knowledge-stat-box .stat-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.knowledge-entities-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.knowledge-entities-list li {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--spacing-sm) 0;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.knowledge-entities-list li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.entity-type-badge {
|
|
display: inline-block;
|
|
padding: 1px 6px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.entity-type-badge.company { background: #dbeafe; color: #1e40af; }
|
|
.entity-type-badge.person { background: #fce7f3; color: #9d174d; }
|
|
.entity-type-badge.place { background: #dcfce7; color: #166534; }
|
|
.entity-type-badge.organization { background: #fef3c7; color: #92400e; }
|
|
.entity-type-badge.project { background: #ede9fe; color: #5b21b6; }
|
|
|
|
.entity-mentions {
|
|
background: var(--background);
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.fact-types-bar {
|
|
display: flex;
|
|
height: 24px;
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
margin-top: var(--spacing-md);
|
|
}
|
|
|
|
.fact-type-segment {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: white;
|
|
transition: flex 0.3s ease;
|
|
min-width: 24px;
|
|
}
|
|
|
|
.fact-type-segment.statistics { background: #3b82f6; }
|
|
.fact-type-segment.investment { background: #10b981; }
|
|
.fact-type-segment.event { background: #f59e0b; }
|
|
.fact-type-segment.decision { background: #ef4444; }
|
|
.fact-type-segment.milestone { background: #8b5cf6; }
|
|
.fact-type-segment.other { background: #6b7280; }
|
|
|
|
.fact-types-legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
margin-top: var(--spacing-sm);
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.fact-types-legend span::before {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 2px;
|
|
margin-right: 4px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.fact-types-legend .ft-statistics::before { background: #3b82f6; }
|
|
.fact-types-legend .ft-investment::before { background: #10b981; }
|
|
.fact-types-legend .ft-event::before { background: #f59e0b; }
|
|
.fact-types-legend .ft-decision::before { background: #ef4444; }
|
|
.fact-types-legend .ft-milestone::before { background: #8b5cf6; }
|
|
.fact-types-legend .ft-other::before { background: #6b7280; }
|
|
|
|
.investment-card {
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
border-left: 3px solid #10b981;
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.investment-value {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 700;
|
|
color: #059669;
|
|
}
|
|
|
|
.investment-desc {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
/* ====== Knowledge Timeline (admin only) ====== */
|
|
.knowledge-timeline {
|
|
position: relative;
|
|
padding: 0 0 0 120px;
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.knowledge-timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 110px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: linear-gradient(180deg, #10b981 0%, #3b82f6 50%, #8b5cf6 100%);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.kt-item {
|
|
position: relative;
|
|
padding-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.kt-item:last-child { padding-bottom: 0; }
|
|
|
|
.kt-date {
|
|
position: absolute;
|
|
left: -120px;
|
|
top: 4px;
|
|
width: 100px;
|
|
text-align: right;
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.kt-dot {
|
|
position: absolute;
|
|
left: -16px;
|
|
top: 6px;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
border: 3px solid white;
|
|
box-shadow: 0 0 0 2px #059669;
|
|
}
|
|
|
|
.kt-dot.investment { background: #10b981; box-shadow: 0 0 0 2px #10b981; }
|
|
.kt-dot.event { background: #3b82f6; box-shadow: 0 0 0 2px #3b82f6; }
|
|
.kt-dot.decision { background: #f59e0b; box-shadow: 0 0 0 2px #f59e0b; }
|
|
.kt-dot.milestone { background: #8b5cf6; box-shadow: 0 0 0 2px #8b5cf6; }
|
|
.kt-dot.statistics { background: #6b7280; box-shadow: 0 0 0 2px #6b7280; }
|
|
|
|
.kt-card {
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
margin-left: var(--spacing-md);
|
|
}
|
|
|
|
.kt-card .fact-type-tag {
|
|
display: inline-block;
|
|
padding: 1px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.kt-card .fact-type-tag.investment { background: #dcfce7; color: #166534; }
|
|
.kt-card .fact-type-tag.event { background: #dbeafe; color: #1e40af; }
|
|
.kt-card .fact-type-tag.decision { background: #fef3c7; color: #92400e; }
|
|
.kt-card .fact-type-tag.milestone { background: #ede9fe; color: #5b21b6; }
|
|
.kt-card .fact-type-tag.statistics { background: #f3f4f6; color: #374151; }
|
|
|
|
.kt-card p {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-primary);
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
}
|
|
|
|
.kt-source {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-muted);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.kt-source a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.kt-source a:hover { text-decoration: underline; }
|
|
|
|
/* ====== Knowledge Graph (admin only) ====== */
|
|
.knowledge-graph-container {
|
|
position: relative;
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.kg-controls {
|
|
position: absolute;
|
|
top: var(--spacing-md);
|
|
left: var(--spacing-md);
|
|
z-index: 10;
|
|
background: rgba(255,255,255,0.95);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.kg-controls label { color: var(--text-secondary); }
|
|
|
|
.kg-controls input[type="range"] {
|
|
width: 100px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.kg-stats {
|
|
position: absolute;
|
|
top: var(--spacing-md);
|
|
right: var(--spacing-md);
|
|
z-index: 10;
|
|
background: rgba(255,255,255,0.95);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.kg-stats strong { color: var(--primary); }
|
|
|
|
#kg-svg {
|
|
width: 100%;
|
|
height: 500px;
|
|
cursor: grab;
|
|
}
|
|
|
|
#kg-svg:active { cursor: grabbing; }
|
|
|
|
.kg-node circle {
|
|
stroke: #fff;
|
|
stroke-width: 2px;
|
|
}
|
|
|
|
.kg-node:hover circle {
|
|
stroke-width: 4px;
|
|
filter: brightness(1.1);
|
|
}
|
|
|
|
.kg-node text {
|
|
font-size: 9px;
|
|
fill: var(--text-primary);
|
|
pointer-events: none;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.kg-link {
|
|
stroke: #999;
|
|
stroke-opacity: 0.4;
|
|
}
|
|
|
|
.kg-legend {
|
|
position: absolute;
|
|
bottom: var(--spacing-md);
|
|
left: var(--spacing-md);
|
|
background: rgba(255,255,255,0.95);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.kg-legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.kg-legend-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.kg-tooltip {
|
|
position: absolute;
|
|
background: rgba(0,0,0,0.85);
|
|
color: white;
|
|
padding: 6px 10px;
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-xs);
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
display: none;
|
|
}
|
|
|
|
.admin-edit-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 12px;
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
background: var(--background);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
text-decoration: none;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.admin-edit-link:hover {
|
|
color: var(--primary);
|
|
border-color: var(--primary);
|
|
background: #eef2ff;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.knowledge-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.knowledge-timeline {
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.knowledge-timeline::before {
|
|
left: 6px;
|
|
}
|
|
|
|
.kt-date {
|
|
position: static;
|
|
width: auto;
|
|
text-align: left;
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.kt-dot {
|
|
left: -20px;
|
|
}
|
|
|
|
.kt-card {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="zopk-hero">
|
|
<div class="container">
|
|
<h1>Zielony Okręg Przemysłowy Kaszubia</h1>
|
|
<p>Strategiczna inicjatywa rozwoju regionu Pomorza - energetyka odnawialna, przemysł obronny, technologie przyszłości. Baza wiedzy o projektach transformacji gospodarczej Kaszub.</p>
|
|
|
|
<!-- Panel ZOPK link moved to admin bar (Treści → ZOP Kaszubia) -->
|
|
<!-- Stats bar removed — numbers without context are confusing for users -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Knowledge Section — hidden from public view, data available in /admin/zopk -->
|
|
{% if false and current_user.is_authenticated and current_user.is_admin and knowledge_data %}
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Wiedza o regionie (baza wiedzy AI)</h2>
|
|
<a href="{{ url_for('admin.admin_zopk_knowledge_dashboard') }}" class="admin-edit-link" title="Zarządzaj bazą wiedzy">✏️ Zarządzaj</a>
|
|
</div>
|
|
|
|
<!-- Stats row -->
|
|
<div class="knowledge-grid">
|
|
<div class="knowledge-stat-box">
|
|
<div class="stat-number">{{ knowledge_data.total_facts }}</div>
|
|
<div class="stat-label">Wyekstrahowane fakty</div>
|
|
</div>
|
|
<div class="knowledge-stat-box">
|
|
<div class="stat-number">{{ knowledge_data.total_entities }}</div>
|
|
<div class="stat-label">Rozpoznane encje</div>
|
|
</div>
|
|
<div class="knowledge-stat-box">
|
|
<div class="stat-number">{{ knowledge_data.total_chunks }}</div>
|
|
<div class="stat-label">Chunki z embeddingami</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fact types bar -->
|
|
{% if knowledge_data.fact_types %}
|
|
{% set total_ft = knowledge_data.fact_types|sum(attribute='1') %}
|
|
{% if total_ft > 0 %}
|
|
<div class="fact-types-bar">
|
|
{% for ft_name, ft_count in knowledge_data.fact_types %}
|
|
<div class="fact-type-segment {{ ft_name or 'other' }}" style="flex: {{ ft_count }}" title="{{ ft_name or 'inne' }}: {{ ft_count }}">
|
|
{% if ft_count * 100 / total_ft > 8 %}{{ ft_count }}{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="fact-types-legend">
|
|
{% for ft_name, ft_count in knowledge_data.fact_types %}
|
|
<span class="ft-{{ ft_name or 'other' }}">{{ ft_name or 'inne' }} ({{ ft_count }})</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<!-- Two columns: entities + investments -->
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-lg); margin-top: var(--spacing-xl);">
|
|
<!-- Top entities -->
|
|
<div>
|
|
<h3 style="font-size: var(--font-size-lg); margin-bottom: var(--spacing-md);">Najczęściej wymieniane encje</h3>
|
|
<ul class="knowledge-entities-list">
|
|
{% for entity in knowledge_data.top_entities %}
|
|
<li>
|
|
<span>
|
|
<span class="entity-type-badge {{ entity.entity_type or 'other' }}">{{ entity.entity_type or '?' }}</span>
|
|
{{ entity.canonical_name or entity.name }}
|
|
</span>
|
|
<span class="entity-mentions">{{ entity.mentions_count }}x</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Key investments / numeric facts -->
|
|
<div>
|
|
<h3 style="font-size: var(--font-size-lg); margin-bottom: var(--spacing-md);">Kluczowe dane liczbowe</h3>
|
|
{% for fact in knowledge_data.key_investments %}
|
|
<div class="investment-card">
|
|
<div class="investment-value">
|
|
{% if fact.numeric_value >= 1000000000 %}
|
|
{{ "%.1f"|format(fact.numeric_value / 1000000000) }} mld {{ fact.numeric_unit or '' }}
|
|
{% elif fact.numeric_value >= 1000000 %}
|
|
{{ "%.1f"|format(fact.numeric_value / 1000000) }} mln {{ fact.numeric_unit or '' }}
|
|
{% elif fact.numeric_value >= 1000 %}
|
|
{{ "%.0f"|format(fact.numeric_value / 1000) }} tys. {{ fact.numeric_unit or '' }}
|
|
{% else %}
|
|
{{ "%.0f"|format(fact.numeric_value) }} {{ fact.numeric_unit or '' }}
|
|
{% endif %}
|
|
</div>
|
|
<div class="investment-desc">{{ fact.full_text[:200] }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Knowledge Timeline — hidden from public view -->
|
|
{% if false and knowledge_data.dated_facts %}
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Oś czasu faktów</h2>
|
|
<a href="{{ url_for('admin.admin_zopk_timeline') }}" class="admin-edit-link" title="Zarządzaj osią czasu">✏️ Zarządzaj</a>
|
|
</div>
|
|
|
|
<div class="knowledge-timeline">
|
|
{% for fact in knowledge_data.dated_facts %}
|
|
<div class="kt-item">
|
|
<div class="kt-date">{{ fact.date_value.strftime('%d.%m.%Y') }}</div>
|
|
<div class="kt-dot {{ fact.fact_type or 'statistics' }}"></div>
|
|
<div class="kt-card">
|
|
<span class="fact-type-tag {{ fact.fact_type or 'statistics' }}">
|
|
{% if fact.fact_type == 'investment' %}Inwestycja
|
|
{% elif fact.fact_type == 'event' %}Wydarzenie
|
|
{% elif fact.fact_type == 'decision' %}Decyzja
|
|
{% elif fact.fact_type == 'milestone' %}Kamień milowy
|
|
{% else %}{{ fact.fact_type or 'Fakt' }}
|
|
{% endif %}
|
|
</span>
|
|
<p>{{ fact.full_text[:300] }}</p>
|
|
{% if fact.source_news %}
|
|
<div class="kt-source">
|
|
Źródło: <a href="{{ fact.source_news.url }}" target="_blank" rel="noopener">{{ fact.source_news.source_name or fact.source_news.source_domain }}</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<!-- Projects Section — only show if there are projects -->
|
|
{% if projects %}
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Projekty strategiczne</h2>
|
|
{% if current_user.is_authenticated and current_user.is_admin %}
|
|
<a href="{{ url_for('admin.admin_zopk') }}" class="admin-edit-link" title="Zarządzaj w panelu ZOPK">✏️ Zarządzaj</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="projects-grid">
|
|
{% for project in projects %}
|
|
<a href="{{ url_for('zopk_project_detail', slug=project.slug) }}" class="project-card" style="border-left-color: {{ project.color or '#059669' }}">
|
|
<div class="project-icon" style="background: {{ project.color or '#059669' }}20; color: {{ project.color or '#059669' }}">
|
|
{% if project.icon == 'wind' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2"/><path d="M9.6 4.6A2 2 0 1 1 11 8H2"/><path d="M12.6 19.4A2 2 0 1 0 14 16H2"/></svg>
|
|
{% elif project.icon == 'atom' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/></svg>
|
|
{% elif project.icon == 'server' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><line x1="6" x2="6.01" y1="6" y2="6"/><line x1="6" x2="6.01" y1="18" y2="18"/></svg>
|
|
{% elif project.icon == 'flask' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 22h12l3-9H3l3 9Z"/><path d="M9 2v8l-3 5"/><path d="M15 2v8l3 5"/><line x1="9" x2="15" y1="2" y2="2"/></svg>
|
|
{% elif project.icon == 'shield' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
{% else %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
|
{% endif %}
|
|
</div>
|
|
<h3>{{ project.name }}</h3>
|
|
<p>{{ project.description[:150] }}{% if project.description|length > 150 %}...{% endif %}</p>
|
|
<span class="project-status status-{{ project.status }}">
|
|
{% if project.status == 'planned' %}Planowany{% elif project.status == 'in_progress' %}W realizacji{% else %}Zakończony{% endif %}
|
|
</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<!-- Timeline / Roadmapa Section -->
|
|
{% if milestones %}
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>🗺️ Roadmapa ZOPK</h2>
|
|
{% if current_user.is_authenticated and current_user.is_admin %}
|
|
<a href="{{ url_for('admin.admin_zopk_timeline') }}" class="admin-edit-link" title="Zarządzaj kamieniami milowymi">✏️ Zarządzaj</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="timeline">
|
|
{% set ns = namespace(current_year=None) %}
|
|
{% for milestone in milestones %}
|
|
{# Year marker when year changes #}
|
|
{% if milestone.target_date %}
|
|
{% set milestone_year = milestone.target_date.year %}
|
|
{% if milestone_year != ns.current_year %}
|
|
{% set ns.current_year = milestone_year %}
|
|
<div class="timeline-year-marker">
|
|
<span class="timeline-year">{{ milestone_year }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
<div class="timeline-item">
|
|
<div class="timeline-icon status-{{ milestone.status }}" title="{{ milestone.status }}">
|
|
{{ milestone.icon or '📌' }}
|
|
</div>
|
|
<div class="timeline-card{% if milestone.is_featured %} featured{% endif %}" style="border-left-color: {{ milestone.color or '#059669' }}">
|
|
<div class="timeline-header">
|
|
<h4>{{ milestone.title }}</h4>
|
|
{% if milestone.target_date %}
|
|
<span class="timeline-date">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>
|
|
{% if milestone.date_precision == 'month' %}
|
|
{{ milestone.target_date.strftime('%m/%Y') }}
|
|
{% elif milestone.date_precision == 'quarter' %}
|
|
Q{{ ((milestone.target_date.month - 1) // 3) + 1 }} {{ milestone.target_date.year }}
|
|
{% elif milestone.date_precision == 'year' %}
|
|
{{ milestone.target_date.year }}
|
|
{% else %}
|
|
{{ milestone.target_date.strftime('%d.%m.%Y') }}
|
|
{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if milestone.description %}
|
|
<p>{{ milestone.description[:200] }}{% if milestone.description|length > 200 %}...{% endif %}</p>
|
|
{% endif %}
|
|
<div class="timeline-meta">
|
|
<span class="timeline-badge badge-{{ milestone.status }}">
|
|
{% if milestone.status == 'completed' %}✅ Zrealizowano
|
|
{% elif milestone.status == 'in_progress' %}🔄 W trakcie
|
|
{% elif milestone.status == 'planned' %}⏳ Planowane
|
|
{% elif milestone.status == 'delayed' %}⚠️ Opóźnione
|
|
{% elif milestone.status == 'cancelled' %}❌ Anulowane
|
|
{% else %}{{ milestone.status }}{% endif %}
|
|
</span>
|
|
<span class="timeline-type">
|
|
{% if milestone.milestone_type == 'announcement' %}📢 Ogłoszenie
|
|
{% elif milestone.milestone_type == 'decision' %}⚖️ Decyzja
|
|
{% elif milestone.milestone_type == 'construction_start' %}🏗️ Start budowy
|
|
{% elif milestone.milestone_type == 'construction_progress' %}🔨 Postęp budowy
|
|
{% elif milestone.milestone_type == 'completion' %}🎉 Ukończenie
|
|
{% elif milestone.milestone_type == 'investment' %}💰 Inwestycja
|
|
{% elif milestone.milestone_type == 'agreement' %}📝 Porozumienie
|
|
{% elif milestone.milestone_type == 'regulation' %}📋 Regulacja
|
|
{% else %}{{ milestone.milestone_type }}{% endif %}
|
|
</span>
|
|
{% if milestone.source_url %}
|
|
<a href="{{ milestone.source_url }}" target="_blank" rel="noopener" class="timeline-source">
|
|
🔗 Źródło
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<!-- Stakeholders Section -->
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Kluczowi interesariusze</h2>
|
|
{% if current_user.is_authenticated and current_user.is_admin %}
|
|
<a href="{{ url_for('admin.admin_zopk') }}" class="admin-edit-link" title="Zarządzaj w panelu ZOPK">✏️ Zarządzaj</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if stakeholders %}
|
|
<div class="stakeholders-list">
|
|
{% for stakeholder in stakeholders %}
|
|
<div class="stakeholder-card">
|
|
<div class="stakeholder-avatar">
|
|
{{ stakeholder.name[0].upper() }}
|
|
</div>
|
|
<div class="stakeholder-info">
|
|
<h4>{{ stakeholder.name }}</h4>
|
|
<p>{{ stakeholder.role or stakeholder.organization }}</p>
|
|
<span class="stakeholder-category category-{{ stakeholder.category }}">
|
|
{% if stakeholder.category == 'government' %}Rząd{% elif stakeholder.category == 'local_authority' %}Samorząd{% elif stakeholder.category == 'business' %}Biznes{% else %}{{ stakeholder.category }}{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak interesariuszy do wyświetlenia.</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- News Section -->
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Aktualności</h2>
|
|
{% if current_user.is_authenticated and current_user.is_admin %}
|
|
<a href="{{ url_for('admin.admin_zopk_news') }}" class="admin-edit-link" title="Zarządzaj aktualnościami">✏️ Zarządzaj</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- News Statistics Bar — hidden, raw numbers don't help users -->
|
|
|
|
{% if news_items %}
|
|
<div class="news-grid">
|
|
{% for news in news_items %}
|
|
<a href="{{ news.url }}" target="_blank" rel="noopener" class="news-card">
|
|
{% if news.image_url %}
|
|
<img src="{{ news.image_url }}" alt="" class="news-image" referrerpolicy="no-referrer"
|
|
onerror="this.onerror=null; this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
|
<div class="news-placeholder" style="display:none;">
|
|
<span class="news-source-initial">{{ (news.source_domain or 'N')[0]|upper }}</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="news-placeholder">
|
|
<span class="news-source-initial">{{ (news.source_domain or 'N')[0]|upper }}</span>
|
|
</div>
|
|
{% endif %}
|
|
<div class="news-content">
|
|
<h4>{{ news.title }}</h4>
|
|
{% if news.description %}
|
|
{% set clean_desc = news.description|striptags %}
|
|
<p>{{ clean_desc[:250] }}{% if clean_desc|length > 250 %}...{% endif %}</p>
|
|
{% endif %}
|
|
<div class="news-meta">
|
|
<span>{{ news.source_name or news.source_domain }}</span>
|
|
<span>{{ news.published_at|local_time('%d.%m.%Y') if news.published_at else '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if news_stats and news_stats.total > 25 %}
|
|
<div style="text-align: center; margin-top: var(--spacing-lg);">
|
|
<a href="{{ url_for('zopk_news_list') }}" class="btn btn-secondary">
|
|
Zobacz starsze aktualności ({{ news_stats.total - 25 }} więcej)
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak aktualności. Wkrótce pojawią się nowe informacje.</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Resources Section -->
|
|
{% if resources %}
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Materiały i dokumenty</h2>
|
|
{% if current_user.is_authenticated and current_user.is_admin %}
|
|
<a href="{{ url_for('admin.admin_zopk') }}" class="admin-edit-link" title="Zarządzaj w panelu ZOPK">✏️ Zarządzaj</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="resources-grid">
|
|
{% for resource in resources %}
|
|
<a href="{{ resource.url or '#' }}" target="_blank" rel="noopener" class="resource-card">
|
|
<div class="resource-icon">
|
|
{% if resource.resource_type == 'document' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
{% elif resource.resource_type == 'video' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect width="15" height="14" x="1" y="5" rx="2" ry="2"/></svg>
|
|
{% elif resource.resource_type == 'image' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
|
|
{% elif resource.resource_type == 'map' %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" x2="8" y1="2" y2="18"/><line x1="16" x2="16" y1="6" y2="22"/></svg>
|
|
{% else %}
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
{% endif %}
|
|
</div>
|
|
<h4>{{ resource.title }}</h4>
|
|
<div class="resource-type">{{ resource.resource_type|capitalize }}</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<!-- Knowledge Graph — hidden from public view -->
|
|
{% if false and current_user.is_authenticated and current_user.is_admin and knowledge_data %}
|
|
<section>
|
|
<div class="section-header">
|
|
<h2>Graf współwystępowania encji</h2>
|
|
<a href="{{ url_for('admin.admin_zopk_knowledge_graph') }}" class="admin-edit-link" title="Zarządzaj grafem relacji">✏️ Zarządzaj</a>
|
|
</div>
|
|
|
|
<div class="knowledge-graph-container">
|
|
<div class="kg-controls">
|
|
<label>Min. współwystąpień:</label>
|
|
<input type="range" id="kgMinCooccur" min="2" max="10" value="2"
|
|
oninput="document.getElementById('kgCooccurVal').textContent=this.value"
|
|
onchange="loadKnowledgeGraph()">
|
|
<span id="kgCooccurVal">2</span>
|
|
</div>
|
|
<div class="kg-stats" id="kgStats">Ładowanie...</div>
|
|
<svg id="kg-svg"></svg>
|
|
<div class="kg-legend">
|
|
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#3b82f6"></div><span>Firmy</span></div>
|
|
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#ec4899"></div><span>Osoby</span></div>
|
|
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#10b981"></div><span>Miejsca</span></div>
|
|
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#f59e0b"></div><span>Organizacje</span></div>
|
|
<div class="kg-legend-item"><div class="kg-legend-dot" style="background:#8b5cf6"></div><span>Projekty</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="kg-tooltip" id="kgTooltip"></div>
|
|
</section>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
{% if false and current_user.is_authenticated and current_user.is_admin and knowledge_data %}
|
|
// Knowledge Graph — D3.js
|
|
(function() {
|
|
if (typeof d3 === 'undefined') {
|
|
window.addEventListener('load', function() { if (typeof d3 !== 'undefined') initKG(); });
|
|
} else {
|
|
initKG();
|
|
}
|
|
|
|
var kgSim, kgSvg, kgG, kgZoom;
|
|
|
|
function initKG() {
|
|
kgSvg = d3.select('#kg-svg');
|
|
if (!kgSvg.node()) return;
|
|
|
|
kgZoom = d3.zoom()
|
|
.scaleExtent([0.1, 4])
|
|
.on('zoom', function(event) { kgG.attr('transform', event.transform); });
|
|
|
|
kgSvg.call(kgZoom);
|
|
kgG = kgSvg.append('g');
|
|
kgG.append('g').attr('class', 'kg-links');
|
|
kgG.append('g').attr('class', 'kg-nodes');
|
|
|
|
loadKnowledgeGraph();
|
|
}
|
|
|
|
window.loadKnowledgeGraph = async function() {
|
|
var minC = document.getElementById('kgMinCooccur').value;
|
|
try {
|
|
var resp = await fetch('/admin/zopk-api/knowledge/graph/data?min_cooccurrence=' + minC + '&limit=150');
|
|
var data = await resp.json();
|
|
if (data.success) {
|
|
renderKG(data.nodes, data.links, data.stats);
|
|
}
|
|
} catch(e) {
|
|
console.error('KG load error:', e);
|
|
}
|
|
};
|
|
|
|
var typeColors = {
|
|
company: '#3b82f6', person: '#ec4899', place: '#10b981',
|
|
organization: '#f59e0b', Organizacja: '#f59e0b',
|
|
project: '#8b5cf6', Projekt: '#8b5cf6',
|
|
Lokalizacja: '#10b981', event: '#ef4444'
|
|
};
|
|
|
|
function nodeColor(type) { return typeColors[type] || '#6b7280'; }
|
|
|
|
function renderKG(nodes, links, stats) {
|
|
var width = kgSvg.node().getBoundingClientRect().width;
|
|
var height = 500;
|
|
|
|
if (kgSim) kgSim.stop();
|
|
kgG.select('.kg-links').selectAll('*').remove();
|
|
kgG.select('.kg-nodes').selectAll('*').remove();
|
|
|
|
document.getElementById('kgStats').innerHTML =
|
|
'<strong>' + stats.total_nodes + '</strong> encji • <strong>' + stats.total_links + '</strong> połączeń';
|
|
|
|
var link = kgG.select('.kg-links').selectAll('line')
|
|
.data(links).enter().append('line')
|
|
.attr('class', 'kg-link')
|
|
.attr('stroke-width', function(d) { return Math.sqrt(d.value); });
|
|
|
|
var node = kgG.select('.kg-nodes').selectAll('g')
|
|
.data(nodes).enter().append('g')
|
|
.attr('class', 'kg-node')
|
|
.call(d3.drag()
|
|
.on('start', function(ev, d) { if (!ev.active) kgSim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
.on('drag', function(ev, d) { d.fx = ev.x; d.fy = ev.y; })
|
|
.on('end', function(ev, d) { if (!ev.active) kgSim.alphaTarget(0); d.fx = null; d.fy = null; }))
|
|
.on('mouseover', function(ev, d) {
|
|
var tt = document.getElementById('kgTooltip');
|
|
tt.innerHTML = '<strong>' + d.name + '</strong><br>Typ: ' + d.type + ' | Wzmianki: ' + d.mentions;
|
|
tt.style.display = 'block';
|
|
tt.style.left = (ev.pageX + 10) + 'px';
|
|
tt.style.top = (ev.pageY + 10) + 'px';
|
|
})
|
|
.on('mouseout', function() { document.getElementById('kgTooltip').style.display = 'none'; });
|
|
|
|
node.append('circle')
|
|
.attr('r', function(d) { return Math.max(6, Math.min(24, Math.sqrt(d.mentions) * 2)); })
|
|
.attr('fill', function(d) { return nodeColor(d.type); });
|
|
|
|
node.filter(function(d) { return d.mentions >= 8; })
|
|
.append('text')
|
|
.attr('dy', function(d) { return Math.max(6, Math.min(24, Math.sqrt(d.mentions) * 2)) + 10; })
|
|
.text(function(d) { return d.name.length > 18 ? d.name.slice(0, 18) + '...' : d.name; });
|
|
|
|
kgSim = d3.forceSimulation(nodes)
|
|
.force('link', d3.forceLink(links).id(function(d) { return d.id; }).distance(80).strength(function(d) { return Math.min(1, d.value / 10); }))
|
|
.force('charge', d3.forceManyBody().strength(-150))
|
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
.force('collision', d3.forceCollide().radius(function(d) { return Math.max(6, Math.sqrt(d.mentions) * 2) + 4; }))
|
|
.on('tick', function() {
|
|
link.attr('x1', function(d) { return d.source.x; })
|
|
.attr('y1', function(d) { return d.source.y; })
|
|
.attr('x2', function(d) { return d.target.x; })
|
|
.attr('y2', function(d) { return d.target.y; });
|
|
node.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
|
|
});
|
|
}
|
|
})();
|
|
{% endif %}
|
|
{% endblock %}
|