nordabiz/templates/classifieds/view.html
Maciej Pienczyn 39da377065
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
fix: UTC timezone correction for all JS date parsing across portal
Added global parseUTC() helper in base.html that appends 'Z' to
naive ISO dates from server. Applied to:
- Notification bell (base.html) — formatTimeAgo
- NordaGPT conversation sort (chat.html)
- B2B interest dates (classifieds/view.html)
- Admin forum moderation dates (admin/forum.html)
- Admin AI insights dates (admin/insights.html)

Same fix as conversations.js parseUTC, now available globally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:09:42 +02:00

1186 lines
40 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}{{ classified.title }} - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.classified-container {
max-width: 800px;
margin: 0 auto;
}
.classified-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.classified-card.szukam {
border-top: 4px solid var(--warning);
}
.classified-card.oferuje {
border-top: 4px solid var(--success);
}
.classified-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-lg);
}
.classified-type {
display: inline-block;
padding: 4px 12px;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-weight: 600;
text-transform: uppercase;
}
.classified-type.szukam {
background: #fef3c7;
color: #92400e;
}
.classified-type.oferuje {
background: #dcfce7;
color: #166534;
}
.classified-category {
display: inline-block;
padding: 4px 12px;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-weight: 500;
text-transform: uppercase;
margin-left: var(--spacing-sm);
}
.category-uslugi {
background: #dbeafe;
color: #1e40af;
}
.category-produkty {
background: #fef3c7;
color: #92400e;
}
.category-wspolpraca {
background: #dcfce7;
color: #166534;
}
.category-praca {
background: #fce7f3;
color: #9d174d;
}
.category-inne {
background: #f3f4f6;
color: #374151;
}
.category-nieruchomosci {
background: #e0e7ff;
color: #3730a3;
}
.classified-title {
font-size: var(--font-size-2xl);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
}
.classified-description {
line-height: 1.8;
color: var(--text-primary);
margin-bottom: var(--spacing-xl);
white-space: pre-wrap;
}
.classified-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
padding: var(--spacing-lg);
background: var(--background);
border-radius: var(--radius);
margin-bottom: var(--spacing-xl);
}
.detail-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
}
.detail-item svg {
width: 20px;
height: 20px;
color: var(--primary);
flex-shrink: 0;
margin-top: 2px;
}
.detail-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.detail-value {
font-weight: 500;
color: var(--text-primary);
}
.author-card {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-lg);
background: var(--background);
border-radius: var(--radius);
}
.author-avatar {
width: 56px;
height: 56px;
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);
}
.author-info {
flex: 1;
}
.author-name {
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.author-company {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.back-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-secondary);
text-decoration: none;
margin-bottom: var(--spacing-lg);
}
.back-link:hover {
color: var(--primary);
}
.stats-bar {
display: flex;
justify-content: space-between;
font-size: var(--font-size-sm);
color: var(--text-secondary);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border);
margin-top: var(--spacing-lg);
}
.seen-by-section {
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border);
}
.seen-by-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.seen-by-avatars {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.reader-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
cursor: default;
position: relative;
}
.reader-avatar[data-name]::after {
content: attr(data-name);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: #1a1a2e;
color: #ffffff;
padding: 6px 12px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
letter-spacing: 0.2px;
}
.reader-avatar[data-name]:hover::after {
opacity: 1;
}
.reader-avatar.more {
background: var(--text-secondary);
font-size: 10px;
}
.close-btn {
margin-left: auto;
}
/* Admin actions */
.admin-actions {
display: flex;
gap: var(--spacing-sm);
margin-left: auto;
}
.admin-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-radius: var(--radius);
font-size: var(--font-size-sm);
font-weight: 500;
cursor: pointer;
border: 1px solid;
transition: all 0.2s;
}
.admin-btn svg {
width: 16px;
height: 16px;
}
.admin-btn-delete {
background: #fef2f2;
color: #dc2626;
border-color: #fecaca;
}
.admin-btn-delete:hover {
background: #fee2e2;
border-color: #f87171;
}
.admin-btn-toggle {
background: #f5f5f5;
color: #525252;
border-color: #d4d4d4;
}
.admin-btn-toggle:hover {
background: #e5e5e5;
border-color: #a3a3a3;
}
.admin-btn-toggle.inactive {
background: #fef3c7;
color: #92400e;
border-color: #fcd34d;
}
.inactive-badge {
background: #fef2f2;
color: #dc2626;
padding: 4px 10px;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-weight: 500;
margin-left: var(--spacing-sm);
}
/* Interest button */
.interest-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 16px;
border-radius: var(--radius);
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-primary);
transition: all 0.2s;
}
.interest-btn:hover {
border-color: var(--primary);
background: #eff6ff;
}
.interest-btn.interested {
background: #dcfce7;
border-color: #22c55e;
color: #166534;
}
.interest-btn svg {
width: 18px;
height: 18px;
}
.interests-info {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-sm);
}
.interests-info a {
color: var(--primary);
text-decoration: none;
}
.interests-info a:hover {
text-decoration: underline;
}
/* Q&A Section */
.qa-section {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.qa-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.qa-header h2 {
font-size: var(--font-size-lg);
font-weight: 600;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.qa-badge {
background: var(--warning);
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: var(--font-size-xs);
font-weight: 600;
}
.qa-form {
background: var(--background);
border-radius: var(--radius);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.qa-form textarea {
width: 100%;
padding: var(--spacing-md);
border: 1px solid var(--border);
border-radius: var(--radius);
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.qa-form textarea:focus {
outline: none;
border-color: var(--primary);
}
.qa-form-actions {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-md);
}
.question-item {
border-bottom: 1px solid var(--border);
padding: var(--spacing-lg) 0;
}
.question-item:last-child {
border-bottom: none;
}
.question-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.question-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.question-meta {
flex: 1;
}
.question-author {
font-weight: 500;
font-size: var(--font-size-sm);
}
.question-date {
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
.question-content {
margin-left: 40px;
line-height: 1.6;
}
.question-hidden {
opacity: 0.5;
}
.question-hidden .question-content::before {
content: "[Ukryte] ";
color: var(--warning);
font-weight: 500;
}
.answer-box {
margin-left: 40px;
margin-top: var(--spacing-md);
padding: var(--spacing-md);
background: #f0fdf4;
border-left: 3px solid #22c55e;
border-radius: 0 var(--radius) var(--radius) 0;
}
.answer-label {
font-size: var(--font-size-xs);
color: #166534;
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.answer-content {
line-height: 1.6;
}
.answer-form {
margin-left: 40px;
margin-top: var(--spacing-md);
}
.answer-form textarea {
width: 100%;
padding: var(--spacing-sm);
border: 1px solid var(--border);
border-radius: var(--radius);
resize: vertical;
min-height: 60px;
font-family: inherit;
font-size: var(--font-size-sm);
}
.answer-form-actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.pending-badge {
display: inline-block;
background: #fef3c7;
color: #92400e;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
margin-left: var(--spacing-sm);
}
.question-actions {
display: flex;
gap: var(--spacing-sm);
}
.question-action-btn {
padding: 4px 8px;
font-size: var(--font-size-xs);
border-radius: var(--radius-sm);
cursor: pointer;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
}
.question-action-btn:hover {
background: var(--background);
}
/* Contact actions */
.contact-actions {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
/* Interests modal */
.interests-list {
max-height: 400px;
overflow-y: auto;
}
.interest-item {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.interest-item:last-child {
border-bottom: none;
}
.interest-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.interest-info {
flex: 1;
}
.interest-name {
font-weight: 500;
}
.interest-company {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.interest-message {
font-size: var(--font-size-sm);
color: var(--text-secondary);
font-style: italic;
margin-top: 4px;
}
.interest-date {
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
.no-questions {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-secondary);
}
</style>
{% endblock %}
{% block content %}
<div class="classified-container">
<a href="{{ url_for('classifieds.classifieds_index') }}" class="back-link">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Powrot do tablicy
</a>
<div class="classified-card {{ classified.listing_type }}">
<div class="classified-header">
<div>
<span class="classified-type {{ classified.listing_type }}">{{ 'Szukam' if classified.listing_type == 'szukam' else 'Oferuje' }}</span>
<span class="classified-category category-{{ classified.category }}">{{ classified.category|replace('uslugi', 'Usługi')|replace('produkty', 'Produkty')|replace('wspolpraca', 'Współpraca')|replace('praca', 'Praca')|replace('inne', 'Inne')|replace('nieruchomosci', 'Nieruchomości') }}</span>
{% if not classified.is_active %}
<span class="inactive-badge">Nieaktywne</span>
{% endif %}
</div>
{% if classified.author_id == current_user.id %}
<button class="btn btn-secondary btn-sm close-btn" onclick="closeClassified()">Zamknij ogloszenie</button>
{% endif %}
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
<div class="admin-actions">
<button type="button" class="admin-btn admin-btn-toggle {% if not classified.is_active %}inactive{% endif %}" onclick="toggleActive()" title="{% if classified.is_active %}Dezaktywuj{% else %}Aktywuj{% endif %}">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
{% if classified.is_active %}
<path d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
{% else %}
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
{% endif %}
</svg>
{% if classified.is_active %}Dezaktywuj{% else %}Aktywuj{% endif %}
</button>
<button type="button" class="admin-btn admin-btn-delete" onclick="deleteClassified()" title="Usuń ogłoszenie">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Usuń
</button>
</div>
{% endif %}
</div>
<h1 class="classified-title">{{ classified.title }}</h1>
<div class="classified-description">{{ classified.description }}</div>
{% if classified.budget_info or classified.location_info %}
<div class="classified-details">
{% if classified.budget_info %}
<div class="detail-item">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<div class="detail-label">Budzet / Cena</div>
<div class="detail-value">{{ classified.budget_info }}</div>
</div>
</div>
{% endif %}
{% if classified.location_info %}
<div class="detail-item">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<div>
<div class="detail-label">Lokalizacja</div>
<div class="detail-value">{{ classified.location_info }}</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
<div class="author-card">
<div class="author-avatar">
{% if classified.author.avatar_path %}<img src="{{ url_for('static', filename=classified.author.avatar_path) }}" alt="" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">{% else %}{{ (classified.author.name or classified.author.email)[0].upper() }}{% endif %}
</div>
<div class="author-info">
<div class="author-name">{{ classified.author.name or classified.author.email.split('@')[0] }}</div>
{% if classified.company %}
<div class="author-company">{{ classified.company.name }}</div>
{% endif %}
</div>
{% if classified.author_id != current_user.id %}
<div class="contact-actions">
<button type="button" class="interest-btn {% if user_interested %}interested{% endif %}" id="interestBtn" onclick="toggleInterest()">
<svg fill="{% if user_interested %}currentColor{% else %}none{% endif %}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/>
</svg>
<span id="interestBtnText">{% if user_interested %}Zainteresowany{% else %}Jestem zainteresowany{% endif %}</span>
</button>
<a href="{{ url_for('messages.conversations_page') }}?new_to={{ classified.author_id }}&ctx=classified&ctx_title={{ classified.title|urlencode }}" class="btn btn-primary">Skontaktuj sie</a>
</div>
{% endif %}
</div>
{% if classified.author_id != current_user.id and interests_count > 0 %}
<div class="interests-info">
{{ interests_count }} {{ 'osoba zainteresowana' if interests_count == 1 else 'osoby zainteresowane' if interests_count < 5 else 'osob zainteresowanych' }}
</div>
{% endif %}
{% if classified.author_id == current_user.id and interests_count > 0 %}
<div class="interests-info">
<a href="#" onclick="showInterestsModal(); return false;">{{ interests_count }} {{ 'osoba zainteresowana' if interests_count == 1 else 'osoby zainteresowane' if interests_count < 5 else 'osob zainteresowanych' }} - zobacz liste</a>
</div>
{% endif %}
<div class="stats-bar">
<span>{{ classified.views_count }} wyswietlen</span>
<span>Dodano: {{ classified.created_at|local_time('%d.%m.%Y %H:%M') }}</span>
{% if classified.expires_at %}
<span>Wygasa: {{ classified.expires_at|local_time('%d.%m.%Y') }}</span>
{% endif %}
</div>
{% if readers %}
<div class="seen-by-section">
<div class="seen-by-label">Widziane przez {{ readers_count }} {{ 'osobę' if readers_count == 1 else 'osoby' if readers_count < 5 else 'osób' }}:</div>
<div class="seen-by-avatars">
{% for read in readers[:20] %}
<div class="reader-avatar"
data-name="{{ read.user.name or read.user.email.split('@')[0] }}{% if current_user.is_authenticated and read.user.id == current_user.id %} (Ty){% endif %}"
style="{% if not read.user.avatar_path %}background: hsl({{ (read.user.id * 137) % 360 }}, 65%, 50%);{% endif %}">
{% if read.user.avatar_path %}<img src="{{ url_for('static', filename=read.user.avatar_path) }}" alt="" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">{% else %}{{ (read.user.name or read.user.email)[0]|upper }}{% endif %}
</div>
{% endfor %}
{% if readers_count > 20 %}
<div class="reader-avatar more" title="i {{ readers_count - 20 }} innych">
+{{ readers_count - 20 }}
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Sekcja Pytania i Odpowiedzi -->
<div class="qa-section">
<div class="qa-header">
<h2>
Pytania i odpowiedzi
{% if classified.author_id == current_user.id and unanswered_count > 0 %}
<span class="qa-badge">{{ unanswered_count }} nowych</span>
{% endif %}
</h2>
</div>
{% if classified.author_id != current_user.id %}
<div class="qa-form">
<textarea id="questionContent" placeholder="Zadaj pytanie sprzedajacemu..." maxlength="2000"></textarea>
<div class="qa-form-actions">
<button type="button" class="btn btn-primary" onclick="askQuestion()">Zadaj pytanie</button>
</div>
</div>
{% endif %}
<div id="questionsList">
{% if questions %}
{% for q in questions %}
<div class="question-item {% if not q.is_public %}question-hidden{% endif %}" id="question-{{ q.id }}">
<div class="question-header">
<div class="question-avatar" style="{% if not q.author.avatar_path %}background: hsl({{ (q.author_id * 137) % 360 }}, 65%, 50%);{% endif %}">
{% if q.author.avatar_path %}<img src="{{ url_for('static', filename=q.author.avatar_path) }}" alt="" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">{% else %}{{ (q.author.name or q.author.email)[0]|upper }}{% endif %}
</div>
<div class="question-meta">
<div class="question-author">
{{ q.author.name or q.author.email.split('@')[0] }}
{% if q.author.company %}<span style="color: var(--text-secondary); font-weight: normal;"> - {{ q.author.company.name }}</span>{% endif %}
{% if not q.answer %}<span class="pending-badge">Oczekuje na odpowiedz</span>{% endif %}
</div>
<div class="question-date">{{ q.created_at|local_time('%d.%m.%Y %H:%M') }}</div>
</div>
{% if classified.author_id == current_user.id %}
<div class="question-actions">
<button type="button" class="question-action-btn" onclick="toggleQuestionVisibility({{ q.id }})" title="{% if q.is_public %}Ukryj{% else %}Pokaz{% endif %}">
{% if q.is_public %}Ukryj{% else %}Pokaz{% endif %}
</button>
</div>
{% endif %}
</div>
<div class="question-content">{{ q.content }}</div>
{% if q.answer %}
<div class="answer-box">
<div class="answer-label">Odpowiedz od {{ classified.author.name or classified.author.email.split('@')[0] }}</div>
<div class="answer-content">{{ q.answer }}</div>
</div>
{% elif classified.author_id == current_user.id %}
<div class="answer-form" id="answerForm-{{ q.id }}">
<textarea id="answerContent-{{ q.id }}" placeholder="Napisz odpowiedz..." maxlength="2000"></textarea>
<div class="answer-form-actions">
<button type="button" class="btn btn-primary btn-sm" onclick="answerQuestion({{ q.id }})">Odpowiedz</button>
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="no-questions">
Brak pytan. {% if classified.author_id != current_user.id %}Badz pierwszy i zadaj pytanie!{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Universal Confirm Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width: 420px; background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl);">
<div style="text-align: center; margin-bottom: var(--spacing-lg);">
<div class="modal-icon" id="confirmModalIcon" style="font-size: 3em; margin-bottom: var(--spacing-md);"></div>
<h3 id="confirmModalTitle" style="margin-bottom: var(--spacing-sm);">Potwierdzenie</h3>
<p class="modal-description" id="confirmModalMessage" style="color: var(--text-secondary);"></p>
</div>
<div class="modal-actions" style="display: flex; gap: var(--spacing-sm); justify-content: center;">
<button type="button" class="btn btn-secondary" id="confirmModalCancel">Anuluj</button>
<button type="button" class="btn btn-primary" id="confirmModalOk">OK</button>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<!-- Interests Modal -->
<div class="modal-overlay" id="interestsModal">
<div class="modal" style="max-width: 500px; background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-lg);">
<h3 style="margin: 0;">Zainteresowani ogłoszeniem</h3>
<button type="button" style="background: none; border: none; font-size: 1.5em; cursor: pointer; color: var(--text-secondary);" onclick="closeInterestsModal()">&times;</button>
</div>
<div class="interests-list" id="interestsList">
<div style="text-align: center; padding: var(--spacing-lg); color: var(--text-secondary);">Ładowanie...</div>
</div>
</div>
</div>
<style>
.modal-overlay#confirmModal, .modal-overlay#interestsModal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1050; align-items: center; justify-content: center; }
.modal-overlay#confirmModal.active, .modal-overlay#interestsModal.active { display: flex; }
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
.btn-danger { background: #dc2626; color: white; border: none; }
.btn-danger:hover { background: #b91c1c; }
.btn-warning { background: #f59e0b; color: white; border: none; }
.btn-warning:hover { background: #d97706; }
.btn-success { background: #10b981; color: white; border: none; }
.btn-success:hover { background: #059669; }
</style>
{% endblock %}
{% block extra_js %}
const csrfToken = '{{ csrf_token() }}';
let confirmResolve = null;
function showConfirm(message, options = {}) {
return new Promise(resolve => {
confirmResolve = resolve;
document.getElementById('confirmModalIcon').textContent = options.icon || '❓';
document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie';
document.getElementById('confirmModalMessage').innerHTML = message;
document.getElementById('confirmModalOk').textContent = options.okText || 'OK';
document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary');
document.getElementById('confirmModal').classList.add('active');
});
}
function closeConfirm(result) {
document.getElementById('confirmModal').classList.remove('active');
if (confirmResolve) { confirmResolve(result); confirmResolve = null; }
}
document.getElementById('confirmModalOk').addEventListener('click', () => closeConfirm(true));
document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false));
document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); });
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
async function closeClassified() {
const confirmed = await showConfirm('Czy na pewno chcesz zamknąć to ogłoszenie?', {
icon: '🔒',
title: 'Zamykanie ogłoszenia',
okText: 'Zamknij',
okClass: 'btn-warning'
});
if (!confirmed) return;
try {
const response = await fetch('{{ url_for("classifieds.classifieds_close", classified_id=classified.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast('Ogłoszenie zostało zamknięte', 'success');
setTimeout(() => window.location.href = '{{ url_for("classifieds.classifieds_index") }}', 1500);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
// Admin functions
async function deleteClassified() {
const confirmed = await showConfirm('Czy na pewno chcesz usunąć to ogłoszenie?<br><br><strong>Ta operacja jest nieodwracalna.</strong>', {
icon: '🗑️',
title: 'Usuń ogłoszenie',
okText: 'Usuń',
okClass: 'btn-danger'
});
if (!confirmed) return;
try {
const response = await fetch('{{ url_for("classifieds.classifieds_delete", classified_id=classified.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast('Ogłoszenie usunięte', 'success');
setTimeout(() => window.location.href = '{{ url_for("classifieds.classifieds_index") }}', 1500);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
async function toggleActive() {
const isActive = {{ 'true' if classified.is_active else 'false' }};
const action = isActive ? 'dezaktywować' : 'aktywować';
const confirmed = await showConfirm(`Czy na pewno chcesz ${action} to ogłoszenie?`, {
icon: isActive ? '🚫' : '✅',
title: isActive ? 'Dezaktywuj ogłoszenie' : 'Aktywuj ogłoszenie',
okText: isActive ? 'Dezaktywuj' : 'Aktywuj',
okClass: isActive ? 'btn-warning' : 'btn-success'
});
if (!confirmed) return;
try {
const response = await fetch('{{ url_for("classifieds.classifieds_toggle_active", classified_id=classified.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
// ============================================================
// INTEREST (ZAINTERESOWANIA)
// ============================================================
async function toggleInterest() {
try {
const response = await fetch('{{ url_for("classifieds.classifieds_interest", classified_id=classified.id) }}', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
const btn = document.getElementById('interestBtn');
const btnText = document.getElementById('interestBtnText');
const svg = btn.querySelector('svg');
if (data.interested) {
btn.classList.add('interested');
btnText.textContent = 'Zainteresowany';
svg.setAttribute('fill', 'currentColor');
} else {
btn.classList.remove('interested');
btnText.textContent = 'Jestem zainteresowany';
svg.setAttribute('fill', 'none');
}
showToast(data.message, 'success');
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
async function showInterestsModal() {
document.getElementById('interestsModal').classList.add('active');
document.getElementById('interestsList').innerHTML = '<div style="text-align: center; padding: var(--spacing-lg); color: var(--text-secondary);">Ładowanie...</div>';
try {
const response = await fetch('{{ url_for("classifieds.classifieds_interests", classified_id=classified.id) }}');
const data = await response.json();
if (data.success) {
if (data.interests.length === 0) {
document.getElementById('interestsList').innerHTML = '<div style="text-align: center; padding: var(--spacing-lg); color: var(--text-secondary);">Brak zainteresowanych</div>';
} else {
document.getElementById('interestsList').innerHTML = data.interests.map(i => `
<div class="interest-item">
<div class="interest-avatar" style="background: hsl(${(i.user_id * 137) % 360}, 65%, 50%);">
${i.user_initial}
</div>
<div class="interest-info">
<div class="interest-name">${i.user_name}</div>
${i.company_name ? `<div class="interest-company">${i.company_name}</div>` : ''}
${i.message ? `<div class="interest-message">"${i.message}"</div>` : ''}
</div>
<div class="interest-date">${parseUTC(i.created_at).toLocaleDateString('pl-PL')}</div>
</div>
`).join('');
}
} else {
document.getElementById('interestsList').innerHTML = '<div style="text-align: center; padding: var(--spacing-lg); color: var(--error);">Błąd ładowania</div>';
}
} catch (error) {
document.getElementById('interestsList').innerHTML = '<div style="text-align: center; padding: var(--spacing-lg); color: var(--error);">Błąd połączenia</div>';
}
}
function closeInterestsModal() {
document.getElementById('interestsModal').classList.remove('active');
}
document.getElementById('interestsModal').addEventListener('click', e => {
if (e.target.id === 'interestsModal') closeInterestsModal();
});
// ============================================================
// Q&A (PYTANIA I ODPOWIEDZI)
// ============================================================
async function askQuestion() {
const content = document.getElementById('questionContent').value.trim();
if (!content) {
showToast('Wpisz treść pytania', 'warning');
return;
}
try {
const response = await fetch('{{ url_for("classifieds.classifieds_ask", classified_id=classified.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ content })
});
const data = await response.json();
if (data.success) {
showToast('Pytanie dodane', 'success');
document.getElementById('questionContent').value = '';
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
async function answerQuestion(questionId) {
const content = document.getElementById(`answerContent-${questionId}`).value.trim();
if (!content) {
showToast('Wpisz treść odpowiedzi', 'warning');
return;
}
try {
const response = await fetch(`/b2b/{{ classified.id }}/question/${questionId}/answer`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ answer: content })
});
const data = await response.json();
if (data.success) {
showToast('Odpowiedź dodana', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
async function toggleQuestionVisibility(questionId) {
try {
const response = await fetch(`/b2b/{{ classified.id }}/question/${questionId}/hide`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
}
{% endblock %}