nordabiz/templates/classifieds/view.html
Maciej Pienczyn 830ef0ea1e feat: Add B2B classifieds interactions (interest, Q&A, context messages)
- Add ClassifiedInterest model for tracking user interest in listings
- Add ClassifiedQuestion model for public Q&A on listings
- Add context_type/context_id to PrivateMessage for B2B linking
- Add interest toggle button and interests list modal
- Add Q&A section with ask/answer/hide functionality
- Update messages to show B2B context badge
- Create migration 034_classified_interactions.sql

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 21:15:30 +01:00

1161 lines
39 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;
}
.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.is_admin %}
<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">
{{ (classified.author.name or classified.author.email)[0].upper() }}
</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_new', to=classified.author_id, context_type='classified', context_id=classified.id) }}" 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.strftime('%d.%m.%Y %H:%M') }}</span>
{% if classified.expires_at %}
<span>Wygasa: {{ classified.expires_at.strftime('%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"
title="{{ read.user.name or read.user.email.split('@')[0] }}{% if current_user.is_authenticated and read.user.id == current_user.id %} (Ty){% endif %}"
style="background: hsl({{ (read.user.id * 137) % 360 }}, 65%, 50%);">
{{ (read.user.name or read.user.email)[0]|upper }}
</div>
{% 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="background: hsl({{ (q.author_id * 137) % 360 }}, 65%, 50%);">
{{ (q.author.name or q.author.email)[0]|upper }}
</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.strftime('%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: {
'Content-Type': 'application/json',
'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">${new Date(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 %}